• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

gcivil-nyu-org / team4-wed-spring25 / 506

16 Apr 2025 10:57PM UTC coverage: 92.942% (-4.8%) from 97.744%
506

Pull #261

travis-pro

web-flow
Merge pull request #260 from gcivil-nyu-org/Han_develop

Han develop
Pull Request #261: Develop

392 of 470 new or added lines in 16 files covered. (83.4%)

2 existing lines in 2 files now uncovered.

1330 of 1431 relevant lines covered (92.94%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.91
/parks/tests.py
1
from django.test import TestCase, Client
1✔
2
from django.urls import reverse
1✔
3
from django.contrib.auth.models import User
1✔
4
from .models import DogRunNew, Review, ParkImage, ReviewReport, ImageReport, Reply
1✔
5
from parks.templatetags.display_rating import render_stars
1✔
6
from parks.templatetags import image_filters
1✔
7
from django.utils.text import slugify
1✔
8
from django.core import mail
1✔
9
from django.contrib.messages import get_messages
1✔
10

11
from django.utils import timezone
1✔
12
from datetime import timedelta
1✔
13
from parks.models import ParkPresence
1✔
14

15
from unittest.mock import patch
1✔
16
from django.core.files.uploadedfile import SimpleUploadedFile
1✔
17

18
from cloudinary import config as cloudinary_config
1✔
19

20

21
@patch(
1✔
22
    "cloudinary.uploader.upload",
23
    return_value={
24
        "asset_id": "dummy_asset_id",
25
        "public_id": "dummy_id",
26
        "version": "1234567890",
27
        "signature": "dummy_signature",
28
        "width": 800,
29
        "height": 600,
30
        "format": "jpg",
31
        "resource_type": "image",
32
        "type": "upload",
33
        "secure_url": "https://res.cloudinary.com/demo/image/upload/sample.jpg",
34
        "url": "https://res.cloudinary.com/demo/image/upload/sample.jpg",
35
    },
36
)
37
class ErrorPageTests(TestCase):
1✔
38
    def test_trigger_400(self, _mock=None):
1✔
39
        response = self.client.get("/test400/")
1✔
40
        self.assertEqual(response.status_code, 400)
1✔
41
        self.assertTemplateUsed(response, "400.html")
1✔
42

43
    def test_trigger_403(self, _mock=None):
1✔
44
        response = self.client.get("/test403/")
1✔
45
        self.assertEqual(response.status_code, 403)
1✔
46
        self.assertTemplateUsed(response, "403.html")
1✔
47

48
    def test_trigger_404(self, _mock=None):
1✔
49
        response = self.client.get("/test404/")
1✔
50
        self.assertEqual(response.status_code, 404)
1✔
51
        self.assertTemplateUsed(response, "404.html")
1✔
52

53
    def test_trigger_500(self, _mock=None):
1✔
54
        response = self.client.get("/test500/")
1✔
55
        self.assertEqual(response.status_code, 500)
1✔
56
        self.assertTemplateUsed(response, "500.html")
1✔
57

58

59
class UniqueEmailTests(TestCase):
1✔
60
    def setUp(self):
1✔
61
        self.client = Client()
1✔
62
        User.objects.create_user(
1✔
63
            username="existinguser",
64
            email="duplicate@pawpark.com",
65
            password="SomeStrongPassword1",
66
        )
67
        self.register_url = reverse("register")
1✔
68

69
    def test_duplicate_email_registration(self):
1✔
70
        """
71
        Attempting to register a new user with an email that already exists should
72
        re-render the form with an error message.
73
        """
74
        response = self.client.post(
1✔
75
            self.register_url,
76
            {
77
                "username": "newuser",
78
                "email": "duplicate@pawpark.com",
79
                "password1": "StrongPass123",
80
                "password2": "StrongPass123",
81
                "role": "user",
82
                "admin_access_code": "",
83
            },
84
        )
85
        self.assertEqual(response.status_code, 200)
1✔
86
        self.assertContains(response, "A user with that email address already exists.")
1✔
87
        self.assertFalse(User.objects.filter(username="newuser").exists())
1✔
88

89

90
class WeakPasswordTests(TestCase):
1✔
91
    def setUp(self):
1✔
92
        self.client = Client()
1✔
93
        self.register_url = reverse("register")
1✔
94

95
    def test_too_short_password(self):
1✔
96
        response = self.client.post(
1✔
97
            self.register_url,
98
            {
99
                "username": "weakuser",
100
                "password1": "123",
101
                "password2": "123",
102
                "role": "user",
103
            },
104
        )
105
        self.assertEqual(response.status_code, 200)
1✔
106
        self.assertFalse(User.objects.filter(username="weakuser").exists())
1✔
107
        self.assertContains(response, "must contain at least 8 characters")
1✔
108

109
    def test_entirely_numeric_password(self):
1✔
110
        response = self.client.post(
1✔
111
            self.register_url,
112
            {
113
                "username": "numericuser",
114
                "password1": "12345678",
115
                "password2": "12345678",
116
                "role": "user",
117
            },
118
        )
119
        self.assertEqual(response.status_code, 200)
1✔
120
        self.assertFalse(User.objects.filter(username="numericuser").exists())
1✔
121
        self.assertContains(response, "can’t be entirely numeric")
1✔
122

123

124
class PasswordResetTests(TestCase):
1✔
125
    def setUp(self):
1✔
126
        self.client = Client()
1✔
127
        self.user = User.objects.create_user(
1✔
128
            "resetuser", "reset@pawpark.com", "Pass123456"
129
        )
130

131
    def test_password_reset_page_loads(self):
1✔
132
        url = reverse("password_reset")
1✔
133
        response = self.client.get(url)
1✔
134
        self.assertEqual(response.status_code, 200)
1✔
135
        self.assertTemplateUsed(response, "registration/password_reset_form.html")
1✔
136

137
    def test_password_reset_flow(self):
1✔
138
        """
139
        Ensure that posting an email to password_reset
140
        sends the user to password_reset_done,
141
        and optionally check that an email was
142
        "sent" (console backend or etc.)
143
        """
144
        url = reverse("password_reset")
1✔
145
        response = self.client.post(url, {"email": "reset@pawpark.com"})
1✔
146
        self.assertEqual(response.status_code, 302)
1✔
147
        self.assertRedirects(response, reverse("password_reset_done"))
1✔
148

149
        self.assertEqual(len(mail.outbox), 1)
1✔
150
        self.assertIn("resetuser", mail.outbox[0].body)
1✔
151

152

153
class AdminSignUpTests(TestCase):
1✔
154
    def setUp(self):
1✔
155
        self.client = Client()
1✔
156
        self.register_url = reverse("register")
1✔
157

158
    def test_admin_signup_with_correct_code(self):
1✔
159
        """
160
        Signing up as admin with correct access code should create a staff user.
161
        """
162
        response = self.client.post(
1✔
163
            self.register_url,
164
            {
165
                "username": "adminuser",
166
                "password1": "StrongAdminPass123",
167
                "password2": "StrongAdminPass123",
168
                "role": "admin",
169
                "admin_access_code": "SUPERDOG123",
170
            },
171
        )
172
        self.assertEqual(response.status_code, 302)
1✔
173
        self.assertTrue(User.objects.filter(username="adminuser").exists())
1✔
174
        user = User.objects.get(username="adminuser")
1✔
175
        self.assertTrue(user.is_staff)
1✔
176

177
    def test_admin_signup_with_wrong_code(self):
1✔
178
        """
179
        Signing up as admin with wrong code should fail and not create staff user.
180
        """
181
        response = self.client.post(
1✔
182
            self.register_url,
183
            {
184
                "username": "fakeadmin",
185
                "password1": "StrongAdminPass123",
186
                "password2": "StrongAdminPass123",
187
                "role": "admin",
188
                "admin_access_code": "WRONGCODE",
189
            },
190
        )
191
        self.assertEqual(response.status_code, 200)
1✔
192
        self.assertFalse(User.objects.filter(username="fakeadmin").exists())
1✔
193

194
    def test_signup_as_normal_user_ignores_access_code(self):
1✔
195
        """
196
        If someone chooses 'user' role, the admin_access_code is irrelevant.
197
        """
198
        response = self.client.post(
1✔
199
            self.register_url,
200
            {
201
                "username": "normaluser",
202
                "password1": "StrongPass456",
203
                "password2": "StrongPass456",
204
                "role": "user",
205
                "admin_access_code": "SUPERDOG123",
206
            },
207
        )
208
        self.assertEqual(response.status_code, 302)
1✔
209
        self.assertTrue(User.objects.filter(username="normaluser").exists())
1✔
210
        user = User.objects.get(username="normaluser")
1✔
211
        self.assertFalse(user.is_staff)
1✔
212

213

214
class LoginTests(TestCase):
1✔
215
    def setUp(self):
1✔
216
        self.client = Client()
1✔
217
        self.user = User.objects.create_user(
1✔
218
            username="testuser", password="StrongPass123"
219
        )
220

221
    def test_login_page_loads(self):
1✔
222
        """Ensure the login page loads properly."""
223
        response = self.client.get(reverse("login"))
1✔
224
        self.assertEqual(response.status_code, 200)
1✔
225
        self.assertTemplateUsed(response, "parks/login.html")
1✔
226

227
    def test_valid_login(self):
1✔
228
        """Ensure a valid user can log in."""
229
        response = self.client.post(
1✔
230
            reverse("login"), {"username": "testuser", "password": "StrongPass123"}
231
        )
232
        self.assertEqual(response.status_code, 302)
1✔
233

234

235
class AuthTests(TestCase):
1✔
236
    def setUp(self):
1✔
237
        self.client = Client()
1✔
238

239
    def test_register_page_loads(self):
1✔
240
        """Ensure the registration page loads properly."""
241
        response = self.client.get(reverse("register"))
1✔
242
        self.assertEqual(response.status_code, 200)
1✔
243
        self.assertTemplateUsed(response, "parks/register.html")
1✔
244

245
    def test_user_registration(self):
1✔
246
        """Ensure a new user can register successfully."""
247
        response = self.client.post(
1✔
248
            reverse("register"),
249
            {
250
                "username": "testuser",
251
                "password1": "StrongPass123",
252
                "password2": "StrongPass123",
253
                "role": "user",  # Ensure this field is required
254
            },
255
        )
256
        self.assertEqual(response.status_code, 302)
1✔
257
        self.assertTrue(User.objects.filter(username="testuser").exists())
1✔
258

259

260
class ParkModelTest(TestCase):
1✔
261
    def setUp(self):
1✔
262
        self.client = Client()
1✔
263
        self.park = DogRunNew.objects.create(
1✔
264
            id="1",
265
            prop_id="1234",
266
            name="Central Park",
267
            address="New York, NY",
268
            dogruns_type="Small",
269
            accessible="Yes",
270
            notes="Test park notes",
271
            google_name="Central Park",
272
            borough="M",
273
            zip_code="United States",
274
            formatted_address="Central Pk N, New York, NY, USA",
275
            latitude=40.7987768,
276
            longitude=-73.9537196,
277
            additional={
278
                "geometry": {
279
                    "bounds": {
280
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
281
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
282
                    },
283
                    "location": {"lat": 40.7987768, "lng": -73.9537196},
284
                    "location_type": "GEOMETRIC_CENTER",
285
                    "viewport": {
286
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
287
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
288
                    },
289
                }
290
            },
291
            display_name="Central Park",
292
            slug=slugify(f"{'Central Park'}-{'1234'}"),
293
        )
294

295
    def test_park_creation(self):
1✔
296
        self.assertEqual(self.park.name, "Central Park")
1✔
297
        self.assertEqual(self.park.address, "New York, NY")
1✔
298
        self.assertEqual(self.park.notes, "Test park notes")
1✔
299
        self.assertEqual(self.park.slug, "central-park-1234")
1✔
300

301

302
class ReviewModelTest(TestCase):
1✔
303
    def setUp(self):
1✔
304
        self.user = User.objects.create_user(username="testuser", password="123456abc")
1✔
305
        self.park = DogRunNew.objects.create(
1✔
306
            id="2",
307
            prop_id="5678",
308
            name="Brooklyn Park",
309
            address="Brooklyn, NY",
310
            dogruns_type="Large",
311
            accessible="No",
312
            notes="Another test park",
313
            display_name="Brooklyn Park",
314
            slug=slugify(f"{'Brooklyn Park'}-{'5678'}"),
315
        )
316
        self.review = Review.objects.create(
1✔
317
            park=self.park, text="Great park!", rating=5, user=self.user
318
        )
319

320
    def test_review_creation(self):
1✔
321
        self.assertEqual(self.review.text, "Great park!")
1✔
322
        self.assertEqual(self.review.rating, 5)
1✔
323
        self.assertEqual(self.review.park.name, "Brooklyn Park")
1✔
324

325
    def test_review_str_method(self):
1✔
326
        self.assertEqual(str(self.review), "Review for Brooklyn Park (5 stars)")
1✔
327

328

329
class CombinedViewTest(TestCase):
1✔
330
    def test_combined_view(self):
1✔
331
        response = self.client.get(reverse("park_and_map"))
1✔
332
        self.assertEqual(response.status_code, 200)
1✔
333

334
    def setUp(self):
1✔
335
        self.client = Client()
1✔
336
        # One park in Manhattan
337
        self.park_manhattan = DogRunNew.objects.create(
1✔
338
            id="1",
339
            prop_id="1234",
340
            name="Central Park",
341
            address="New York, NY",
342
            dogruns_type="Small",
343
            accessible="Yes",
344
            notes="Manhattan park",
345
            google_name="Central Park",
346
            borough="M",
347
            zip_code="10024",
348
            latitude=40.7987768,
349
            longitude=-73.9537196,
350
            display_name="Central Park",
351
        )
352
        # One park in Brooklyn
353
        self.park_brooklyn = DogRunNew.objects.create(
1✔
354
            id="2",
355
            prop_id="5678",
356
            name="Brooklyn Bridge Park",
357
            address="Brooklyn, NY",
358
            dogruns_type="Large",
359
            accessible="Yes",
360
            notes="Brooklyn park",
361
            google_name="Brooklyn Bridge Park",
362
            borough="B",
363
            zip_code="11201",
364
            latitude=40.700292,
365
            longitude=-73.996123,
366
            display_name="Brooklyn Bridge Park",
367
        )
368

369
    def test_combined_view_filters_by_borough(self):
1✔
370
        response = self.client.get(reverse("park_and_map"), {"borough": "M"})
1✔
371
        self.assertEqual(response.status_code, 200)
1✔
372
        self.assertContains(response, "Central Park")
1✔
373
        self.assertNotContains(response, "Brooklyn Bridge Park")
1✔
374

375

376
class ParkDetailViewTest(TestCase):
1✔
377
    def setUp(self):
1✔
378
        """Set up the test client and create a test park."""
379
        self.client = Client()
1✔
380

381
        self.park = DogRunNew.objects.create(
1✔
382
            id="1",
383
            prop_id="1234",
384
            name="Central Park",
385
            address="New York, NY",
386
            dogruns_type="Small",
387
            accessible="Yes",
388
            notes="Test park notes",
389
            google_name="Central Park",
390
            borough="M",
391
            zip_code="United States",
392
            formatted_address="Central Pk N, New York, NY, USA",
393
            latitude=40.7987768,
394
            longitude=-73.9537196,
395
            additional={
396
                "geometry": {
397
                    "bounds": {
398
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
399
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
400
                    },
401
                    "location": {"lat": 40.7987768, "lng": -73.9537196},
402
                    "location_type": "GEOMETRIC_CENTER",
403
                    "viewport": {
404
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
405
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
406
                    },
407
                }
408
            },
409
            display_name="Central Park",
410
            slug="central-park-1234",
411
        )
412

413
        self.park2 = DogRunNew.objects.create(
1✔
414
            id="2",
415
            prop_id="4321",
416
            name="Allison Pond Park",
417
            address="Staten Island",
418
            dogruns_type="Small",
419
            accessible="Yes",
420
            notes="Test park notes",
421
            google_name="Allison Pond Park",
422
            borough="Q",
423
            zip_code="United States",
424
            formatted_address="Allison Pond Park, Staten Island, NY 10301, USA",
425
            latitude=40.7987768,
426
            longitude=-73.9537196,
427
            display_name="Allison Pond Park",
428
            slug="allison-pond-park-4321",
429
        )
430

431
    def test_park_detail_page_loads(self):
1✔
432
        url = self.park.detail_page_url()
1✔
433
        response = self.client.get(url)
1✔
434
        self.assertEqual(response.status_code, 200)
1✔
435
        self.assertTemplateUsed(response, "parks/park_detail.html")
1✔
436
        self.assertContains(response, "Central Park")
1✔
437

438
    def test_redirect_on_wrong_slug(self):
1✔
439
        url = reverse("park_detail", kwargs={"slug": "wrong-slug", "id": self.park.id})
1✔
440
        response = self.client.get(url)
1✔
441
        self.assertEqual(response.status_code, 301)
1✔
442

443
        correct_response = url = reverse(
1✔
444
            "park_detail", kwargs={"slug": self.park.slug, "id": self.park.id}
445
        )
446

447
        self.assertRedirects(response, correct_response, status_code=301)
1✔
448

449
    def test_404_on_nonexistent_id(self):
1✔
450
        url = reverse("park_detail", kwargs={"slug": "central-park", "id": "-4"})
1✔
451
        response = self.client.get(url)
1✔
452
        self.assertEqual(response.status_code, 404)
1✔
453

454
    def test_redirect_on_wrong_id_right_slug(self):
1✔
455
        url = reverse(
1✔
456
            "park_detail", kwargs={"slug": "central-park", "id": self.park2.id}
457
        )
458
        response = self.client.get(url)
1✔
459

460
        self.assertEqual(response.status_code, 301)
1✔
461

462
        expected_url = reverse(
1✔
463
            "park_detail", kwargs={"slug": self.park2.slug, "id": self.park2.id}
464
        )
465
        self.assertRedirects(
1✔
466
            response, expected_url, status_code=301, target_status_code=200
467
        )
468

469
    from django.contrib.messages import get_messages
1✔
470

471

472
def test_submit_review_non_integer_rating(self):
1✔
NEW
473
    self.client.login(username="testuser", password="testpass")
×
474

NEW
475
    response = self.client.post(
×
476
        self.park.detail_page_url(),
477
        {
478
            "form_type": "submit_review",
479
            "text": "This should not go through.",
480
            "rating": "abc",
481
        },
482
        follow=True,
483
    )
484

NEW
485
    self.assertEqual(response.status_code, 200)
×
486

NEW
487
    messages = list(get_messages(response.wsgi_request))
×
NEW
488
    self.assertTrue(
×
489
        any("Please select a rating before submitting." in str(m) for m in messages),
490
        "Expected error message not found in messages.",
491
    )
492

493

494
class ReportFunctionalityTests(TestCase):
1✔
495
    def setUp(self):
1✔
496
        self.client = Client()
1✔
497
        self.user = User.objects.create_user(
1✔
498
            username="reporter", password="testpass123"
499
        )
500
        self.other_user = User.objects.create_user(
1✔
501
            username="uploader", password="testpass123"
502
        )
503

504
        self.park = DogRunNew.objects.create(
1✔
505
            id="10",
506
            prop_id="9999",
507
            name="Test Park",
508
            address="Test Address",
509
            dogruns_type="All",
510
            accessible="Yes",
511
            formatted_address="Test Address",
512
            latitude=40.0,
513
            longitude=-73.0,
514
        )
515

516
        self.image = ParkImage.objects.create(
1✔
517
            park=self.park,
518
            image="https://res.cloudinary.com/demo/image/upload/sample.jpg",
519
            user=self.other_user,
520
        )
521

522
        self.review = Review.objects.create(
1✔
523
            park=self.park, text="Nice place!", rating=4, user=self.other_user
524
        )
525

526
        self.client.login(username="reporter", password="testpass123")
1✔
527

528
    def test_report_image_creates_record(self):
1✔
529
        response = self.client.post(
1✔
530
            reverse("report_image", args=[self.image.id]),
531
            {"reason": "Inappropriate image"},
532
        )
533
        self.assertEqual(response.status_code, 302)
1✔
534
        self.assertEqual(self.image.reports.count(), 1)
1✔
535
        report = self.image.reports.first()
1✔
536
        self.assertEqual(report.reason, "Inappropriate image")
1✔
537
        self.assertEqual(report.user, self.user)
1✔
538

539
    def test_report_review_creates_record(self):
1✔
540
        response = self.client.post(
1✔
541
            reverse("park_detail", args=[self.park.slug, self.park.id]),
542
            {
543
                "form_type": "report_review",
544
                "review_id": self.review.id,
545
                "reason": "Offensive content",
546
            },
547
        )
548
        self.assertEqual(response.status_code, 302)
1✔
549
        self.assertEqual(self.review.reports.count(), 1)
1✔
550
        report = self.review.reports.first()
1✔
551
        self.assertEqual(report.reason, "Offensive content")
1✔
552
        self.assertEqual(report.reported_by, self.user)
1✔
553

554
    def test_submit_review(self):
1✔
555
        response = self.client.post(
1✔
556
            reverse("park_detail", args=[self.park.slug, self.park.id]),
557
            {"form_type": "submit_review", "text": "Another review!", "rating": "5"},
558
        )
559
        self.assertEqual(response.status_code, 302)
1✔
560
        self.assertEqual(Review.objects.filter(park=self.park).count(), 2)
1✔
561

562
    def test_review_report_str(self):
1✔
563
        report = ReviewReport.objects.create(
1✔
564
            review=self.review, reported_by=self.user, reason="Inappropriate content"
565
        )
566
        self.assertIn("Reported by", str(report))
1✔
567
        self.assertIn(str(self.review.id), str(report))
1✔
568

569
    def test_image_report_str(self):
1✔
570
        report = ImageReport.objects.create(
1✔
571
            image=self.image, user=self.user, reason="Offensive image"
572
        )
573
        self.assertIn("Report by", str(report))
1✔
574
        self.assertIn(str(self.image.id), str(report))
1✔
575

576
    def test_missing_reason_does_not_create_report(self):
1✔
577
        self.client.login(username="user2", password="testpass")
1✔
578
        response = self.client.post(
1✔
579
            reverse("report_image", args=[self.image.id]),
580
            {"reason": ""},
581
        )
582
        self.assertEqual(ImageReport.objects.count(), 0)
1✔
583
        self.assertEqual(response.status_code, 302)
1✔
584

585
    def test_duplicate_review_report(self):
1✔
586
        # First report
587
        response1 = self.client.post(
1✔
588
            reverse("park_detail", args=[self.park.slug, self.park.id]),
589
            {
590
                "form_type": "report_review",
591
                "review_id": self.review.id,
592
                "reason": "Spam",
593
            },
594
        )
595
        self.assertEqual(response1.status_code, 302)
1✔
596
        self.assertEqual(self.review.reports.count(), 1)
1✔
597

598
        # Second report by same user
599
        response2 = self.client.post(
1✔
600
            reverse("park_detail", args=[self.park.slug, self.park.id]),
601
            {
602
                "form_type": "report_review",
603
                "review_id": self.review.id,
604
                "reason": "Still spam",
605
            },
606
        )
607
        self.assertEqual(response2.status_code, 302)
1✔
608
        self.assertEqual(self.review.reports.count(), 1)  # should still be 1
1✔
609

610
    def test_duplicate_image_report(self):
1✔
611
        # First report
612
        response1 = self.client.post(
1✔
613
            reverse("report_image", args=[self.image.id]),
614
            {"reason": "Bad image"},
615
        )
616
        self.assertEqual(response1.status_code, 302)
1✔
617
        self.assertEqual(self.image.reports.count(), 1)
1✔
618

619
        # Second report by same user
620
        response2 = self.client.post(
1✔
621
            reverse("report_image", args=[self.image.id]),
622
            {"reason": "Still bad"},
623
        )
624
        self.assertEqual(response2.status_code, 302)
1✔
625
        self.assertEqual(self.image.reports.count(), 1)  # should still be 1
1✔
626

627

628
class DeleteTests(TestCase):
1✔
629
    def setUp(self):
1✔
630
        self.client = Client()
1✔
631
        self.user = User.objects.create_user(username="deleter", password="123pass")
1✔
632
        self.client.login(username="deleter", password="123pass")
1✔
633

634
        self.park = DogRunNew.objects.create(
1✔
635
            id="22",
636
            prop_id="9988",
637
            name="Del Park",
638
            address="Somewhere",
639
            dogruns_type="All",
640
            accessible="Yes",
641
            formatted_address="Addr",
642
            latitude=40.0,
643
            longitude=-73.0,
644
        )
645
        self.review = Review.objects.create(
1✔
646
            park=self.park, text="Review", rating=4, user=self.user
647
        )
648
        self.image = ParkImage.objects.create(
1✔
649
            park=self.park,
650
            image="https://res.cloudinary.com/demo/image/upload/sample.jpg",
651
            user=self.user,
652
        )
653

654
    def test_delete_review(self):
1✔
655
        response = self.client.post(reverse("delete_review", args=[self.review.id]))
1✔
656
        self.assertEqual(response.status_code, 302)
1✔
657
        self.assertFalse(Review.objects.filter(id=self.review.id).exists())
1✔
658

659
    def test_delete_image(self):
1✔
660
        response = self.client.post(reverse("delete_image", args=[self.image.id]))
1✔
661
        self.assertEqual(response.status_code, 302)
1✔
662
        self.assertFalse(ParkImage.objects.filter(id=self.image.id).exists())
1✔
663

664

665
class ParkImageModelTest(TestCase):
1✔
666
    def setUp(self):
1✔
667
        """Set up a test park and associated images."""
668
        self.park = DogRunNew.objects.create(
1✔
669
            id="1",
670
            prop_id="1234",
671
            name="Central Park",
672
            address="New York, NY",
673
            dogruns_type="Small",
674
            accessible="Yes",
675
            notes="Test park notes",
676
            google_name="Central Park",
677
            borough="M",
678
            zip_code="United States",
679
            formatted_address="Central Pk N, New York, NY, USA",
680
            latitude=40.7987768,
681
            longitude=-73.9537196,
682
            additional={
683
                "geometry": {
684
                    "bounds": {
685
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
686
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
687
                    },
688
                    "location": {"lat": 40.7987768, "lng": -73.9537196},
689
                    "location_type": "GEOMETRIC_CENTER",
690
                    "viewport": {
691
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
692
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
693
                    },
694
                }
695
            },
696
        )
697
        self.image = ParkImage.objects.create(
1✔
698
            park=self.park,
699
            image="https://res.cloudinary.com/demo/image/upload/sample.jpg",
700
        )
701

702
    def test_park_image_creation(self):
1✔
703
        """Test that a ParkImage object is created successfully."""
704
        self.assertEqual(self.image.park, self.park)
1✔
705
        self.assertEqual(
1✔
706
            self.image.image, "https://res.cloudinary.com/demo/image/upload/sample.jpg"
707
        )
708

709
    def test_park_image_str(self):
1✔
710
        """Test the string representation of a ParkImage object."""
711
        self.assertEqual(str(self.image), f"Image for {self.park.name}")
1✔
712

713

714
class ParkDetailViewImageTest(TestCase):
1✔
715
    def setUp(self):
1✔
716
        """Set up a test park and associated images."""
717
        self.client = Client()
1✔
718
        self.park = DogRunNew.objects.create(
1✔
719
            id="1",
720
            prop_id="1234",
721
            name="Central Park",
722
            address="New York, NY",
723
            dogruns_type="Small",
724
            accessible="Yes",
725
            notes="Test park notes",
726
            google_name="Central Park",
727
            borough="M",
728
            zip_code="United States",
729
            formatted_address="Central Pk N, New York, NY, USA",
730
            latitude=40.7987768,
731
            longitude=-73.9537196,
732
            additional={
733
                "geometry": {
734
                    "bounds": {
735
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
736
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
737
                    },
738
                    "location": {"lat": 40.7987768, "lng": -73.9537196},
739
                    "location_type": "GEOMETRIC_CENTER",
740
                    "viewport": {
741
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
742
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
743
                    },
744
                }
745
            },
746
        )
747
        self.image = ParkImage.objects.create(
1✔
748
            park=self.park,
749
            image="https://res.cloudinary.com/demo/image/upload/sample.jpg",
750
        )
751

752
    def test_park_detail_view_with_images(self):
1✔
753
        """Test that the park detail view displays associated images."""
754
        response = self.client.get(
1✔
755
            reverse("park_detail", args=[self.park.slug, self.park.id])
756
        )
757
        self.assertEqual(response.status_code, 200)
1✔
758
        self.assertContains(response, self.park.name)
1✔
759
        # self.assertIn(self.image.image, response.content.decode())
760

761

762
class ParkDetailDisplayedReviewsTests(TestCase):
1✔
763
    def setUp(self):
1✔
764
        self.client = Client()
1✔
765
        self.user = User.objects.create_user(username="reviewer", password="pass123")
1✔
766

767
        self.park = DogRunNew.objects.create(
1✔
768
            id="10",
769
            prop_id="PARK100",
770
            name="Test Park",
771
            address="100 Test St",
772
            dogruns_type="Run",
773
            accessible="Yes",
774
            notes="Some notes",
775
            google_name="Test Park",
776
            borough="M",
777
            zip_code="10001",
778
            formatted_address="100 Test St, New York, NY",
779
            latitude=40.7128,
780
            longitude=-74.0060,
781
            display_name="Test Park",
782
            slug=slugify("Test Park-PARK100"),
783
        )
784

785
        # One visible review
786
        self.review_visible = Review.objects.create(
1✔
787
            park=self.park,
788
            user=self.user,
789
            text="This park is great!",
790
            rating=5,
791
            is_removed=False,
792
        )
793

794
        # One soft-deleted review
795
        self.review_removed = Review.objects.create(
1✔
796
            park=self.park,
797
            user=self.user,
798
            text="This review should be hidden",
799
            rating=1,
800
            is_removed=True,
801
        )
802

803
    def test_only_visible_reviews_displayed(self):
1✔
804
        url = reverse("park_detail", args=[self.park.slug, self.park.id])
1✔
805
        response = self.client.get(url)
1✔
806

807
        self.assertEqual(response.status_code, 200)
1✔
808
        self.assertContains(response, self.review_visible.text)
1✔
809
        self.assertNotContains(response, self.review_removed.text)
1✔
810

811
    def test_average_rating_excludes_removed_reviews(self):
1✔
812
        url = reverse("park_detail", args=[self.park.slug, self.park.id])
1✔
813
        response = self.client.get(url)
1✔
814

815
        # Ensure the average is based only on the 5-star review
816
        self.assertContains(response, "5.0")
1✔
817

818

819
class RenderStarsTests(TestCase):
1✔
820
    def test_int_stars(self):
1✔
821
        size = 20
1✔
822
        result = render_stars(4, size)
1✔
823
        self.assertEqual(result["filled_stars"], 4)
1✔
824
        self.assertEqual(result["half_stars"], 0)
1✔
825
        self.assertEqual(result["empty_stars"], 1)
1✔
826
        self.assertEqual(result["size"], size)
1✔
827

828
    def test_full_stars(self):
1✔
829
        size = 15
1✔
830
        result = render_stars(5, size)
1✔
831
        self.assertEqual(result["filled_stars"], 5)
1✔
832
        self.assertEqual(result["half_stars"], 0)
1✔
833
        self.assertEqual(result["empty_stars"], 0)
1✔
834
        self.assertEqual(result["size"], size)
1✔
835

836
    def test_no_stars(self):
1✔
837
        size = 10
1✔
838
        result = render_stars(0, size)
1✔
839
        self.assertEqual(result["filled_stars"], 0)
1✔
840
        self.assertEqual(result["half_stars"], 0)
1✔
841
        self.assertEqual(result["empty_stars"], 5)
1✔
842
        self.assertEqual(result["size"], size)
1✔
843

844
    def test_half_stars(self):
1✔
845
        size = 20
1✔
846
        result = render_stars(2.5, size)
1✔
847
        self.assertEqual(result["filled_stars"], 2)
1✔
848
        self.assertEqual(result["half_stars"], 1)
1✔
849
        self.assertEqual(result["empty_stars"], 2)
1✔
850
        self.assertEqual(result["size"], size)
1✔
851

852
    # >= X.25 -> one half star
853
    def test_round_up_to_half(self):
1✔
854
        size = 20
1✔
855
        result = render_stars(4.25, size)
1✔
856
        self.assertEqual(result["filled_stars"], 4)
1✔
857
        self.assertEqual(result["half_stars"], 1)
1✔
858
        self.assertEqual(result["empty_stars"], 0)
1✔
859
        self.assertEqual(result["size"], size)
1✔
860

861
    # < X.25 -> round down to whole
862
    def test_round_down_to_whole(self):
1✔
863
        size = 20
1✔
864
        result = render_stars(3.24, size)
1✔
865
        self.assertEqual(result["filled_stars"], 3)
1✔
866
        self.assertEqual(result["half_stars"], 0)
1✔
867
        self.assertEqual(result["empty_stars"], 2)
1✔
868
        self.assertEqual(result["size"], size)
1✔
869

870
    # < X.75 -> one half star
871
    def test_round_down_to_half(self):
1✔
872
        size = 20
1✔
873
        result = render_stars(2.74, size)
1✔
874
        self.assertEqual(result["filled_stars"], 2)
1✔
875
        self.assertEqual(result["half_stars"], 1)
1✔
876
        self.assertEqual(result["empty_stars"], 2)
1✔
877
        self.assertEqual(result["size"], size)
1✔
878

879
    # >= X.75 -> round up to next whole
880
    def test_round_up_to_whole(self):
1✔
881
        size = 20
1✔
882
        result = render_stars(4.75, size)
1✔
883
        self.assertEqual(result["filled_stars"], 5)
1✔
884
        self.assertEqual(result["half_stars"], 0)
1✔
885
        self.assertEqual(result["empty_stars"], 0)
1✔
886
        self.assertEqual(result["size"], size)
1✔
887

888

889
class ParkPresenceTests(TestCase):
1✔
890
    def setUp(self):
1✔
891
        self.client = Client()
1✔
892
        self.user = User.objects.create_user(username="tester", password="testpass")
1✔
893
        self.park = DogRunNew.objects.create(
1✔
894
            id="5",
895
            prop_id="5566",
896
            name="Test Dog Park",
897
            address="Test Location",
898
            dogruns_type="All",
899
            accessible="Yes",
900
            formatted_address="Test Address",
901
            latitude=40.0,
902
            longitude=-73.0,
903
            display_name="Test Dog Park",
904
            slug="test-dog-park-5566",
905
        )
906
        self.client.login(username="tester", password="testpass")
1✔
907

908
    def test_user_check_in_creates_presence(self):
1✔
909
        self.client.post(
1✔
910
            reverse("park_detail", args=[self.park.slug, self.park.id]),
911
            {"form_type": "check_in"},
912
        )
913
        presences = ParkPresence.objects.filter(user=self.user, park=self.park)
1✔
914
        self.assertEqual(presences.count(), 1)
1✔
915
        self.assertEqual(presences.first().status, "current")
1✔
916

917
    def test_user_be_there_at_creates_presence(self):
1✔
918
        future_time = (timezone.now() + timedelta(minutes=20)).strftime("%H:%M")
1✔
919
        self.client.post(
1✔
920
            reverse("park_detail", args=[self.park.slug, self.park.id]),
921
            {"form_type": "be_there_at", "time": future_time},
922
        )
923
        presences = ParkPresence.objects.filter(user=self.user, park=self.park)
1✔
924
        self.assertEqual(presences.count(), 1)
1✔
925
        self.assertEqual(presences.first().status, "on_the_way")
1✔
926

927

928
@patch(
1✔
929
    "cloudinary.uploader.upload",
930
    return_value={
931
        "asset_id": "dummy_asset_id",
932
        "public_id": "dummy_id",
933
        "version": "1234567890",
934
        "signature": "dummy_signature",
935
        "width": 800,
936
        "height": 600,
937
        "format": "jpg",
938
        "resource_type": "image",
939
        "type": "upload",
940
        "secure_url": "https://dummy.cloudinary.com/image.jpg",
941
        "url": "http://dummy.cloudinary.com/image.jpg",
942
    },
943
)
944
class ImageUploadTests(TestCase):
1✔
945
    def setUp(self):
1✔
946
        self.client = Client()
1✔
947
        self.user = User.objects.create_user(username="uploader", password="pass123")
1✔
948
        self.client.login(username="uploader", password="pass123")
1✔
949
        self.park = DogRunNew.objects.create(
1✔
950
            id="20",
951
            prop_id="8888",
952
            name="Mock Park",
953
            address="123",
954
            dogruns_type="All",
955
            accessible="Yes",
956
            formatted_address="123",
957
            latitude=40.0,
958
            longitude=-73.0,
959
            slug="mock-park-8888",
960
            display_name="Mock Park",
961
        )
962

963
    def test_upload_image_with_review(self, mock_upload):
1✔
964
        cloudinary_config(
1✔
965
            cloud_name="demo",
966
            api_key="fake_api_key",
967
            api_secret="fake_api_secret",
968
        )
969

970
        image = SimpleUploadedFile(
1✔
971
            "test.jpg", b"file_content", content_type="image/jpeg"
972
        )
973

974
        response = self.client.post(
1✔
975
            reverse("park_detail", args=[self.park.slug, self.park.id]),
976
            {
977
                "form_type": "submit_review",
978
                "text": "Nice park!",
979
                "rating": "5",
980
                "images": image,
981
            },
982
            follow=True,
983
        )
984

985
        self.assertEqual(response.status_code, 200)
1✔
986
        self.assertEqual(ParkImage.objects.count(), 1)
1✔
987

988

989
class ModalInteractionTests(TestCase):
1✔
990
    def test_modal_js_is_present(self):
1✔
991
        client = Client()
1✔
992
        park = DogRunNew.objects.create(
1✔
993
            id="7",
994
            prop_id="3344",
995
            name="Modal Park",
996
            address="JSville",
997
            dogruns_type="Small",
998
            accessible="Yes",
999
            formatted_address="JS Road",
1000
            latitude=42.0,
1001
            longitude=-75.0,
1002
            display_name="Modal Park",
1003
            slug="modal-park-3344",
1004
        )
1005
        response = client.get(reverse("park_detail", args=[park.slug, park.id]))
1✔
1006
        self.assertContains(response, "function openCarouselImageModal")
1✔
1007
        self.assertContains(response, "imagePreviewModal")
1✔
1008
        self.assertContains(response, "modalImage")
1✔
1009

1010

1011
class ReplaceFilterTests(TestCase):
1✔
1012
    def test_replace_basic(self):
1✔
1013
        result = image_filters.replace("hello world", "world,there")
1✔
1014
        self.assertEqual(result, "hello there")
1✔
1015

1016
    def test_replace_partial_match(self):
1✔
1017
        result = image_filters.replace("abcabcabc", "a,x")
1✔
1018
        self.assertEqual(result, "xbcxbcxbc")
1✔
1019

1020
    def test_replace_only_first_comma_splits(self):
1✔
1021
        result = image_filters.replace("one,two,three", "two,2")
1✔
1022
        self.assertEqual(result, "one,2,three")
1✔
1023

1024
    def test_replace_with_comma_in_replacement(self):
1✔
1025
        result = image_filters.replace("item1,item2", "item1,x,y")
1✔
1026
        self.assertEqual(result, "x,y,item2")  # Splits only on first comma
1✔
1027

1028
    def test_replace_no_match(self):
1✔
1029
        result = image_filters.replace("hello", "z,x")
1✔
1030
        self.assertEqual(result, "hello")
1✔
1031

1032

1033
class ReplyViewTests(TestCase):
1✔
1034
    def setUp(self):
1✔
1035
        self.client = Client()
1✔
1036
        self.user = User.objects.create_user(username="testuser", password="testpass")
1✔
1037
        self.park = DogRunNew.objects.create(
1✔
1038
            id="99",
1039
            prop_id="9999",
1040
            name="Reply Park",
1041
            address="Somewhere",
1042
            dogruns_type="All",
1043
            accessible="Yes",
1044
            formatted_address="Reply Address",
1045
            latitude=40.0,
1046
            longitude=-73.0,
1047
            slug="reply-park-9999",
1048
        )
1049
        self.review = Review.objects.create(
1✔
1050
            park=self.park, text="Original Review", rating=4, user=self.user
1051
        )
1052
        self.park_detail_url = reverse(
1✔
1053
            "park_detail", args=[self.park.slug, self.park.id]
1054
        )
1055

1056
    def test_submit_reply_to_review(self):
1✔
1057
        self.client.login(username="testuser", password="testpass")
1✔
1058
        response = self.client.post(
1✔
1059
            self.park_detail_url,
1060
            {
1061
                "form_type": "submit_reply",
1062
                "parent_review_id": self.review.id,
1063
                "reply_text": "This is a reply to a review.",
1064
            },
1065
        )
1066
        self.assertEqual(response.status_code, 302)
1✔
1067
        self.assertTrue(
1✔
1068
            Reply.objects.filter(
1069
                review=self.review, text="This is a reply to a review."
1070
            ).exists()
1071
        )
1072

1073
    def test_submit_nested_reply_to_reply(self):
1✔
1074
        parent_reply = Reply.objects.create(
1✔
1075
            review=self.review, user=self.user, text="Parent reply"
1076
        )
1077
        self.client.login(username="testuser", password="testpass")
1✔
1078
        response = self.client.post(
1✔
1079
            self.park_detail_url,
1080
            {
1081
                "form_type": "submit_reply",
1082
                "parent_review_id": self.review.id,
1083
                "parent_reply_id": parent_reply.id,
1084
                "reply_text": "Child reply",
1085
            },
1086
        )
1087
        self.assertEqual(response.status_code, 302)
1✔
1088
        self.assertTrue(
1✔
1089
            Reply.objects.filter(parent_reply=parent_reply, text="Child reply").exists()
1090
        )
1091

1092
    def test_submit_reply_with_invalid_parent_reply_id(self):
1✔
1093
        self.client.login(username="testuser", password="testpass")
1✔
1094
        response = self.client.post(
1✔
1095
            self.park_detail_url,
1096
            {
1097
                "form_type": "submit_reply",
1098
                "parent_review_id": self.review.id,
1099
                "parent_reply_id": 9999,
1100
                "reply_text": "Fallback to review",
1101
            },
1102
        )
1103
        self.assertEqual(response.status_code, 302)
1✔
1104
        reply = Reply.objects.get(text="Fallback to review")
1✔
1105
        self.assertIsNone(reply.parent_reply)
1✔
1106

1107
    def test_submit_reply_without_text(self):
1✔
1108
        self.client.login(username="testuser", password="testpass")
1✔
1109
        response = self.client.post(
1✔
1110
            self.park_detail_url,
1111
            {
1112
                "form_type": "submit_reply",
1113
                "parent_review_id": self.review.id,
1114
                "reply_text": "   ",
1115
            },
1116
        )
1117
        self.assertEqual(response.status_code, 302)
1✔
1118
        self.assertEqual(Reply.objects.filter(review=self.review).count(), 0)
1✔
1119

1120
    def test_submit_reply_unauthenticated(self):
1✔
1121
        response = self.client.post(
1✔
1122
            self.park_detail_url,
1123
            {
1124
                "form_type": "submit_reply",
1125
                "parent_review_id": self.review.id,
1126
                "reply_text": "Unauthorized reply",
1127
            },
1128
        )
1129
        self.assertEqual(response.status_code, 200)
1✔
1130
        self.assertNotIn("Reply submitted successfully", response.content.decode())
1✔
1131

1132
    def test_delete_own_reply(self):
1✔
1133
        self.client.login(username="testuser", password="testpass")
1✔
1134
        reply = Reply.objects.create(
1✔
1135
            review=self.review, user=self.user, text="To be deleted"
1136
        )
1137
        response = self.client.post(reverse("delete_reply", args=[reply.id]))
1✔
1138
        self.assertEqual(response.status_code, 302)
1✔
1139
        self.assertFalse(Reply.objects.filter(id=reply.id).exists())
1✔
1140

1141
    def test_delete_others_reply_forbidden(self):
1✔
1142
        other = User.objects.create_user(username="other", password="pass")
1✔
1143
        reply = Reply.objects.create(review=self.review, user=other, text="Not yours")
1✔
1144
        self.client.login(username="testuser", password="testpass")
1✔
1145
        response = self.client.post(reverse("delete_reply", args=[reply.id]))
1✔
1146
        self.assertEqual(response.status_code, 302)
1✔
1147
        self.assertTrue(Reply.objects.filter(id=reply.id).exists())
1✔
1148

1149
    def test_report_reply_success(self):
1✔
1150
        other = User.objects.create_user(username="other", password="pass")
1✔
1151
        reply = Reply.objects.create(review=self.review, user=other, text="Report me")
1✔
1152
        self.client.login(username="testuser", password="testpass")
1✔
1153
        response = self.client.post(
1✔
1154
            reverse("report_reply", args=[reply.id]), {"reason": "Spam"}
1155
        )
1156
        self.assertEqual(response.status_code, 302)
1✔
1157
        self.assertTrue(reply.reports.exists())
1✔
1158

1159
    def test_report_own_reply_fails(self):
1✔
1160
        reply = Reply.objects.create(
1✔
1161
            review=self.review, user=self.user, text="Self report"
1162
        )
1163
        self.client.login(username="testuser", password="testpass")
1✔
1164
        response = self.client.post(
1✔
1165
            reverse("report_reply", args=[reply.id]), {"reason": "Oops"}
1166
        )
1167
        self.assertEqual(response.status_code, 302)
1✔
1168
        self.assertEqual(reply.reports.count(), 0)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc