Skip to content

Conversation

@ArnavBallinCode
Copy link
Member

@ArnavBallinCode ArnavBallinCode commented Dec 13, 2025

Adding tests for as mentioned in the pr #1406

Summary by Sourcery

Add API test fixtures and a concurrency test to verify refund race condition handling.

Tests:

  • Introduce shared API test fixtures for organizers, events, auth clients, and orders.
  • Add a concurrent refund race condition test ensuring only one of two overlapping refund requests succeeds and total refunded amount remains consistent.
  • Initialize the API tests package for discovery by the test runner.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Dec 13, 2025

Reviewer's Guide

Adds a new API test module with shared fixtures and a concurrency-focused refund race condition test to verify that the select_for_update-based locking in OrderPaymentViewSet.refund() prevents double refunds under concurrent requests.

File-Level Changes

Change Details Files
Introduce shared API test fixtures for organizers, events, auth clients, and orders with payments to support API test scenarios.
  • Add pytest fixtures for organizer, event, team, device, and user setup with scopes_disabled where DB writes occur.
  • Provide authenticated API clients via user, team token, and device authentication to simplify API tests.
  • Create reusable event-related fixtures (item, tax rule, order with confirmed payment, fee, and order position) for consistent test data.
  • Wrap django.test.utils.setup_databases with scopes_disabled to avoid scope restrictions during DB setup in tests.
app/eventyay/api/tests/conftest.py
Add a concurrent refund race condition test to ensure only one refund succeeds when two concurrent refund requests exceed the payment amount.
  • Implement TestConcurrentRefunds test class using transactional DB to exercise concurrency behavior.
  • Spawn two threads that each POST an $80 refund for a $100 confirmed payment against the refund API endpoint using a team token for auth.
  • Collect and assert that one response is 200 and the other 400, with no thread errors and exactly one refund created totaling $80.
  • Ensure database connections are closed per thread and payment state is refreshed with scopes_disabled to inspect resulting refunds.
app/eventyay/api/tests/test_concurrent_refunds.py
Mark the API tests package as a Python package.
  • Add an empty init.py file to the api tests directory.
app/eventyay/api/tests/__init__.py

Possibly linked issues

  • #unknown: The PR adds new tests, directly contributing to the issue’s request for more test coverage in this repo.
  • #(not specified): The PR adds a concurrency test confirming the refund-overpayment race condition described in the issue is prevented.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • Overriding django.test.utils.setup_databases globally in conftest.py with a scopes_disabled wrapper may have unintended side effects on other test modules; consider scoping this behavior to specific tests or a dedicated test settings module instead.
  • The thread worker in test_concurrent_refunds_race_condition_prevented uses the plain django.test.Client rather than the configured DRF APIClient; consider reusing the same client type or a small helper to avoid subtle differences in request handling or auth behavior.
  • Shared results and errors lists are mutated from multiple threads without synchronization; while list appends are atomic in CPython, using a Queue or similar thread-safe container would make the concurrency semantics clearer and more robust.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Overriding `django.test.utils.setup_databases` globally in `conftest.py` with a `scopes_disabled` wrapper may have unintended side effects on other test modules; consider scoping this behavior to specific tests or a dedicated test settings module instead.
- The thread worker in `test_concurrent_refunds_race_condition_prevented` uses the plain `django.test.Client` rather than the configured DRF `APIClient`; consider reusing the same client type or a small helper to avoid subtle differences in request handling or auth behavior.
- Shared `results` and `errors` lists are mutated from multiple threads without synchronization; while list appends are atomic in CPython, using a `Queue` or similar thread-safe container would make the concurrency semantics clearer and more robust.

## Individual Comments

### Comment 1
<location> `app/eventyay/api/tests/test_concurrent_refunds.py:67-76` </location>
<code_context>
+        def make_refund_request(amount):
</code_context>

<issue_to_address>
**suggestion (testing):** Add synchronization (e.g. `threading.Barrier`) so both threads start at the same time and reliably exercise the race condition.

As written, `thread1.start()` and `thread2.start()` let each thread send its HTTP request as soon as it starts, so one request can finish before the other even begins. That makes the race coverage unreliable and can cause flaky pass/fail results. Using a `threading.Barrier` (or similar) to block both threads until they’re ready, then releasing them together, will make the test more deterministic.

Suggested implementation:

```python
        def make_refund_request(amount, start_barrier):
            """Make a refund request in a separate thread."""
            try:
                # Synchronize all worker threads so they start the HTTP request at (roughly) the same time
                start_barrier.wait()

                client = Client()
                client.defaults['HTTP_AUTHORIZATION'] = auth_header
                response = client.post(

```

To fully implement the synchronization and make the race test deterministic, you should also:

1. Import `threading` at the top of this test file:
   - Add `import threading` alongside the other imports.

2. Create a barrier sized to the number of concurrent worker threads right before starting them, for example:
   - `start_barrier = threading.Barrier(2)` (if there are exactly two threads in this test).

3. Pass the barrier into each thread's `args` when constructing them, updating any existing thread creation such as:
   - From: `threading.Thread(target=make_refund_request, args=(Decimal("50.00"),))`
   - To:   `threading.Thread(target=make_refund_request, args=(Decimal("50.00"), start_barrier))`

This ensures both threads block on `start_barrier.wait()` and only proceed to send their HTTP requests once all of them are ready, making the race condition reliably exercised and reducing test flakiness.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds comprehensive testing infrastructure for concurrent refund operations, specifically to validate the fix for a race condition that could allow double refunds. The test suite ensures that when two concurrent refund requests are made that would exceed the available payment amount, only one succeeds due to proper database locking mechanisms.

  • Introduces shared test fixtures (organizers, events, orders, payments) for API testing
  • Adds a multi-threaded concurrency test that simulates race conditions in refund processing
  • Validates that select_for_update() prevents double refunds by ensuring only one of two overlapping requests succeeds

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
app/eventyay/api/tests/init.py Package initialization file for API tests
app/eventyay/api/tests/conftest.py Comprehensive pytest fixtures providing test data for organizers, events, teams, authenticated clients, and orders with payments
app/eventyay/api/tests/test_concurrent_refunds.py Concurrency test validating race condition prevention in refund operations using threading to simulate simultaneous requests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Sak1012 Sak1012 requested a review from hongquan December 17, 2025 12:54
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if thread1.is_alive() or thread2.is_alive():
pytest.fail(
f"Test threads did not finish within timeout. "
f"{{thread1 alive: {{thread1.is_alive()}}}}, thread2 alive: {{thread2.is_alive()}}}}"
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The f-string contains double curly braces which will be printed literally instead of interpolating the variables. The braces inside an f-string should be single to enable variable interpolation.

Suggested change
f"{{thread1 alive: {{thread1.is_alive()}}}}, thread2 alive: {{thread2.is_alive()}}}}"
f"thread1 alive: {thread1.is_alive()}, thread2 alive: {thread2.is_alive()}"

Copilot uses AI. Check for mistakes.
Comment on lines +160 to +165
o.payments.create(
provider='manual',
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
amount=Decimal('100.00'),
payment_date=datetime(2025, 1, 1, 10, 0, 0, tzinfo=timezone.utc),
)
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses the 'manual' payment provider which does not support refunds by default. The BasePaymentProvider.payment_partial_refund_supported method returns False, and ManualPayment doesn't override it. This will cause both refund requests to fail with a 400 error at lines 1280-1283 of api/views/order.py (since the test attempts partial refunds of 80 from a 100 payment), rather than testing the intended race condition behavior. Consider using a payment provider that supports partial refunds, such as creating a test-specific payment provider that overrides payment_partial_refund_supported to return True.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +57
client.defaults['HTTP_AUTHORIZATION'] = auth_header
response = client.post(
f'/api/v1/organizers/{organizer.slug}/events/{event.slug}/'
f'orders/{order.code}/payments/{payment.local_id}/refund/',
data=json.dumps({'amount': str(amount)}),
content_type='application/json',
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Django's test Client does not support setting headers via client.defaults. The correct way to pass headers to Django test Client's post method is to pass them as keyword arguments directly to the post call. The line should be changed to pass HTTP_AUTHORIZATION as a parameter to the post method.

Suggested change
client.defaults['HTTP_AUTHORIZATION'] = auth_header
response = client.post(
f'/api/v1/organizers/{organizer.slug}/events/{event.slug}/'
f'orders/{order.code}/payments/{payment.local_id}/refund/',
data=json.dumps({'amount': str(amount)}),
content_type='application/json',
response = client.post(
f'/api/v1/organizers/{organizer.slug}/events/{event.slug}/'
f'orders/{order.code}/payments/{payment.local_id}/refund/',
data=json.dumps({'amount': str(amount)}),
content_type='application/json',
HTTP_AUTHORIZATION=auth_header,

Copilot uses AI. Check for mistakes.
Copy link
Member

@mariobehling mariobehling left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check latest AI comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants