Skip to content

test: add integration tests for campaign launch flow#304

Open
bh462007 wants to merge 2 commits into
Kuldeeep18:mainfrom
bh462007:test/campaign-launch-integration-tests
Open

test: add integration tests for campaign launch flow#304
bh462007 wants to merge 2 commits into
Kuldeeep18:mainfrom
bh462007:test/campaign-launch-integration-tests

Conversation

@bh462007

@bh462007 bh462007 commented Jun 17, 2026

Copy link
Copy Markdown

Summary

This PR adds comprehensive integration test coverage for the campaign launch endpoint (POST /api/v1/campaigns/{id}/launch/).

A new CampaignLaunchTests test class has been added to backend/campaigns/tests.py to validate the endpoint's behavior across both success and failure paths, including campaign validation rules and Celery execution modes.

No production code was modified as part of this change.

Closes #291

What Was Added

Created a dedicated CampaignLaunchTests suite covering the following scenarios:

Validation & Error Cases

  • Launching a campaign without a connected account and without enrolled leads
  • Launching with a connected account that belongs to a different organization
  • Launching with a connected account owned by another user
  • Launching a campaign that has zero enrolled leads

For all invalid launch attempts, the API returns the expected error response and the campaign remains in the DRAFT state.

Successful Launch Cases

  • Launching a properly configured campaign with a valid connected account and enrolled leads
  • Re-launching an already ACTIVE campaign to verify existing behavior

Celery Execution Modes

  • Launching with CELERY_TASK_ALWAYS_EAGER=True to verify leads are processed synchronously and no async task is dispatched
  • Launching in normal async mode to verify process_active_leads.delay() is triggered correctly

Files Modified

  • backend/campaigns/tests.py

    • Added a new CampaignLaunchTests integration test suite

Testing

All campaign tests pass successfully after adding the new coverage:

Found 46 test(s).
----------------------------------------------------------------------
Ran 46 tests in 14.275s

OK

Run locally with:

cd backend
python3 manage.py test campaigns

Type of Change

  • Testing (new integration tests, no production code changes)

Checklist

  • Added coverage for all scenarios described in the issue
  • Verified behavior in both eager and asynchronous Celery modes
  • Used existing Django test fixtures and patterns
  • Confirmed no existing tests were affected
  • Full campaigns test suite passes successfully

Summary by CodeRabbit

  • New Features

    • Active filter badges now display on the leads page for tags, date ranges, and status.
    • CSV upload success confirmation with automatic page refresh added.
  • Improvements

    • Enhanced empty-state messaging for leads and import history tables.
    • Import error modal now clearly indicates when uploads complete without row-level errors.
  • Tests

    • Added comprehensive campaign launch workflow tests covering error scenarios, status transitions, and processing modes.

Copilot AI review requested due to automatic review settings June 17, 2026 18:26
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a CampaignLaunchTests class with eight integration tests covering the campaign launch endpoint's validation, status transitions, idempotent relaunching, and Celery eager/async dispatch. Separately, frontend/leads.html is refactored with data-loader functions, an expanded filter panel (tags, dates, status), updated empty states, conditional import-error view buttons, and a finalized CSV 202 upload path.

Changes

Campaign Launch Integration Tests

Layer / File(s) Summary
Test fixtures and helper methods
backend/campaigns/tests.py
CampaignLaunchTests.setUp creates an org, user, authenticated client, and connected email account. _make_campaign and _enroll_lead helpers support test setup.
Launch endpoint test cases
backend/campaigns/tests.py
Eight test methods cover missing/wrong-org/wrong-user account (400), zero enrolled leads (400), valid launch to ACTIVE (200), idempotent relaunch on an already-ACTIVE campaign, eager-mode inline processing, and async-mode Celery task dispatch via patched process_active_leads.delay.

Leads Page JS/HTML Refactor

Layer / File(s) Summary
Filter panel, table, and modal HTML structure
frontend/leads.html
Filter toggle button, breadcrumb, filter panel sections (tags, date inputs, status radios, action controls, active-filter summary row), section boundaries around import history/leads tables, and import errors modal comment marker are updated and normalized.
Client state, utilities, and render functions
frontend/leads.html
State declarations (leads, tags, importJobs, selectedTagIds, activeFilters) are rewritten. contrastColor and formatTimestamp utilities are added. renderImportErrorRows, openImportErrors, renderLeadRows, and renderImportHistory are updated with empty states and conditional "View" button logic for failed import rows.
Filter params, indicator, and apply logic
frontend/leads.html
Tag chip click handler is normalized. getFilterParams builds query params from selectedTagIds, date inputs, and status radio. updateFilterIndicator renders badge chips per active filter type and hides the row when empty. applyFilters stores activeFilters, updates the indicator, fetches filtered leads, and re-renders stats and table.
Data loaders, bootstrap, and CSV upload
frontend/leads.html
loadLeads and loadImportHistory data-loader functions are added. DOMContentLoaded bootstrap is rewritten to call loaders first, then wire tags, search, refresh, and filter-panel toggle. CSV upload submit handler is re-anchored; 202 success path shows a message and reloads the page after a delay.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 Hop, hop — the tests are laid,
Eight launch paths precisely weighed.
The filter chips now glow in rows,
Empty states where no lead goes.
A 202 arrives at last —
The CSV lands and reloads fast! 🌟

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes out-of-scope changes to frontend/leads.html that are unrelated to the campaign launch integration tests specified in issue #291. Remove the frontend/leads.html changes from this PR; they should be addressed in a separate pull request focused on frontend updates.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding integration tests for the campaign launch flow endpoint.
Linked Issues check ✅ Passed The PR fully implements all 8 required test scenarios from issue #291, covering validation errors, successful launches, and both Celery execution modes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR updates the Leads frontend to include import history/error visibility and adds backend API tests covering campaign launch validation and task dispatch behavior.

Changes:

  • Adds an “Import History” table and an “Import Errors” modal to the leads page UI.
  • Resolves merge-conflict artifacts and refactors parts of the leads page JS (filters/search/import history rendering).
  • Adds a new CampaignLaunchTests suite to validate /campaigns/{id}/launch/ behavior across common failure/success paths.

Reviewed changes

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

File Description
frontend/leads.html Adds import history + error modal UI and related JS helpers while cleaning up prior merge conflict changes.
backend/campaigns/tests.py Adds comprehensive API tests around campaign launch preconditions and Celery dispatch behavior.
Comments suppressed due to low confidence (1)

frontend/leads.html:1

  • This uses innerHTML with server-provided values (e.g., job.filename, job.status) without escaping, which can enable XSS if any field contains HTML. Prefer inserting via textContent/DOM APIs, or wrap interpolated server-provided strings with escapeHtml() before concatenating into innerHTML.
<!DOCTYPE html>

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

Comment thread frontend/leads.html
Comment on lines 510 to 512
function openImportErrors(jobId) {
const job = allImportJobs.find(item => item.id === jobId);
if (!job) return;
def test_launch_without_connected_account_and_no_leads_returns_400(self):
"""Campaign with no connected account and no leads should return 400."""
campaign = self._make_campaign()
response = self.client.post(f'/api/v1/campaigns/{campaign.id}/launch/', {}, format='json')
response = self.client.post(f'/api/v1/campaigns/{campaign.id}/launch/', {}, format='json')

self.assertEqual(response.status_code, status.HTTP_200_OK)
mocked_once.assert_called()

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/leads.html (2)

501-507: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

XSS vulnerability: entry.email and entry.error are rendered without escaping.

These values originate from CSV import validation and can contain attacker-controlled content. A malicious CSV with an email like <img src=x onerror=alert(1)> would execute JavaScript when viewing errors.

🔒 Proposed fix
             tbody.innerHTML = errors.map(entry => `
                 <tr>
                     <td class="fw-semibold">${entry.row ?? '-'}</td>
-                    <td>${entry.email || '<span class="text-muted">-</span>'}</td>
-                    <td>${entry.error || '<span class="text-muted">-</span>'}</td>
+                    <td>${entry.email ? escapeHtml(entry.email) : '<span class="text-muted">-</span>'}</td>
+                    <td>${entry.error ? escapeHtml(entry.error) : '<span class="text-muted">-</span>'}</td>
                 </tr>
             `).join('');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/leads.html` around lines 501 - 507, The entry.email and entry.error
properties are being directly inserted into the HTML template without HTML
escaping, creating an XSS vulnerability where malicious content from CSV imports
can execute JavaScript. Create or use a helper function to HTML-escape these
values (replacing special characters like <, >, &, ", ') before they are
interpolated into the template string. Apply this escaping to both entry.email
and entry.error in the template literal where tbody.innerHTML is being set.

574-587: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

XSS vulnerability: job.filename is rendered without escaping.

Filenames are user-controlled. An attacker could upload a file named <script>alert(1)</script>.csv to execute arbitrary JavaScript.

🔒 Proposed fix
             tbody.innerHTML = jobs.map(job => `
                 <tr>
-                    <td class="fw-semibold">${job.filename}</td>
+                    <td class="fw-semibold">${escapeHtml(job.filename)}</td>
                     <td>${job.total_rows ?? 0}</td>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/leads.html` around lines 574 - 587, The `job.filename` value is
being rendered directly into the HTML template without escaping, creating an XSS
vulnerability where filenames containing malicious code (like
`<script>alert(1)</script>.csv`) could execute arbitrary JavaScript. Escape the
HTML entities in the `job.filename` variable before inserting it into the
template string by using a helper function that replaces special characters such
as `<`, `>`, `&`, `"`, and `'` with their corresponding HTML entities. Apply
this escaping specifically to the `${job.filename}` interpolation in the
template literal used in the tbody.innerHTML assignment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/campaigns/tests.py`:
- Around line 1434-1440: The test
`test_launch_without_connected_account_and_no_leads_returns_400` currently tests
two invalid conditions simultaneously, so it passes via the zero-leads
validation check and never actually verifies the missing-connected-account
requirement. To isolate the missing-connected-account failure path, enroll at
least one lead in the campaign (using an appropriate method to add a lead to the
campaign object before making the API call) so the request reaches the
connected-account validation logic rather than failing early on the leads check.

In `@frontend/leads.html`:
- Around line 510-517: The `openImportErrors` function has a type mismatch where
`jobId` parameter is a string (from `btn.dataset.jobId`) but `item.id` from the
API is a number, causing the strict equality check in the find method to always
fail silently. Convert `jobId` to a number using `parseInt` or the unary plus
operator before comparing it with `item.id` in the find condition to ensure the
types match and the job lookup succeeds.

---

Outside diff comments:
In `@frontend/leads.html`:
- Around line 501-507: The entry.email and entry.error properties are being
directly inserted into the HTML template without HTML escaping, creating an XSS
vulnerability where malicious content from CSV imports can execute JavaScript.
Create or use a helper function to HTML-escape these values (replacing special
characters like <, >, &, ", ') before they are interpolated into the template
string. Apply this escaping to both entry.email and entry.error in the template
literal where tbody.innerHTML is being set.
- Around line 574-587: The `job.filename` value is being rendered directly into
the HTML template without escaping, creating an XSS vulnerability where
filenames containing malicious code (like `<script>alert(1)</script>.csv`) could
execute arbitrary JavaScript. Escape the HTML entities in the `job.filename`
variable before inserting it into the template string by using a helper function
that replaces special characters such as `<`, `>`, `&`, `"`, and `'` with their
corresponding HTML entities. Apply this escaping specifically to the
`${job.filename}` interpolation in the template literal used in the
tbody.innerHTML assignment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 56ee7144-e664-4dca-87bd-26ba9c204050

📥 Commits

Reviewing files that changed from the base of the PR and between 90052f9 and 3000086.

📒 Files selected for processing (2)
  • backend/campaigns/tests.py
  • frontend/leads.html

Comment on lines +1434 to +1440
def test_launch_without_connected_account_and_no_leads_returns_400(self):
"""Campaign with no connected account and no leads should return 400."""
campaign = self._make_campaign()
response = self.client.post(f'/api/v1/campaigns/{campaign.id}/launch/', {}, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('error', response.data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Isolate the missing-connected-account failure path

Line 1434 currently combines two invalid conditions (no connected account + no enrolled leads), so this test passes via the zero-leads branch and does not actually verify the “missing connected account” requirement. Please enroll a lead here (or rename the test to reflect zero-leads validation) so coverage matches the intended scenario.

Suggested test adjustment
-    def test_launch_without_connected_account_and_no_leads_returns_400(self):
-        """Campaign with no connected account and no leads should return 400."""
+    def test_launch_without_connected_account_returns_400(self):
+        """Campaign with enrolled leads but no sender account should return 400."""
         campaign = self._make_campaign()
+        self._enroll_lead(campaign)
         response = self.client.post(f'/api/v1/campaigns/{campaign.id}/launch/', {}, format='json')
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertIn('error', response.data)
+        campaign.refresh_from_db()
+        self.assertEqual(campaign.status, 'DRAFT')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/campaigns/tests.py` around lines 1434 - 1440, The test
`test_launch_without_connected_account_and_no_leads_returns_400` currently tests
two invalid conditions simultaneously, so it passes via the zero-leads
validation check and never actually verifies the missing-connected-account
requirement. To isolate the missing-connected-account failure path, enroll at
least one lead in the campaign (using an appropriate method to add a lead to the
campaign object before making the API call) so the request reaches the
connected-account validation logic rather than failing early on the leads check.

Comment thread frontend/leads.html
Comment on lines 510 to 517
function openImportErrors(jobId) {
const job = allImportJobs.find(item => item.id === jobId);
if (!job) return;

document.getElementById('import-errors-meta').textContent =
`${job.filename} · ${job.failed_count} error${job.failed_count === 1 ? '' : 's'}`;
renderImportErrorRows(job);
bootstrap.Modal.getOrCreateInstance(document.getElementById('importErrorsModal')).show();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Type mismatch: jobId is a string but item.id is likely a number.

btn.dataset.jobId always returns a string, but job.id from the API is typically a number. The strict equality check item.id === jobId will always fail, causing the "View" button to silently do nothing.

🐛 Proposed fix
         function openImportErrors(jobId) {
-            const job = allImportJobs.find(item => item.id === jobId);
+            const job = allImportJobs.find(item => String(item.id) === jobId);
             if (!job) return;

Alternatively, parse to number if IDs are always numeric:

         function openImportErrors(jobId) {
-            const job = allImportJobs.find(item => item.id === jobId);
+            const job = allImportJobs.find(item => item.id === Number(jobId));
             if (!job) return;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function openImportErrors(jobId) {
const job = allImportJobs.find(item => item.id === jobId);
if (!job) return;
document.getElementById('import-errors-meta').textContent =
`${job.filename} · ${job.failed_count} error${job.failed_count === 1 ? '' : 's'}`;
renderImportErrorRows(job);
bootstrap.Modal.getOrCreateInstance(document.getElementById('importErrorsModal')).show();
}
function openImportErrors(jobId) {
const job = allImportJobs.find(item => String(item.id) === jobId);
if (!job) return;
document.getElementById('import-errors-meta').textContent =
`${job.filename} · ${job.failed_count} error${job.failed_count === 1 ? '' : 's'}`;
renderImportErrorRows(job);
bootstrap.Modal.getOrCreateInstance(document.getElementById('importErrorsModal')).show();
}
Suggested change
function openImportErrors(jobId) {
const job = allImportJobs.find(item => item.id === jobId);
if (!job) return;
document.getElementById('import-errors-meta').textContent =
`${job.filename} · ${job.failed_count} error${job.failed_count === 1 ? '' : 's'}`;
renderImportErrorRows(job);
bootstrap.Modal.getOrCreateInstance(document.getElementById('importErrorsModal')).show();
}
function openImportErrors(jobId) {
const job = allImportJobs.find(item => item.id === Number(jobId));
if (!job) return;
document.getElementById('import-errors-meta').textContent =
`${job.filename} · ${job.failed_count} error${job.failed_count === 1 ? '' : 's'}`;
renderImportErrorRows(job);
bootstrap.Modal.getOrCreateInstance(document.getElementById('importErrorsModal')).show();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/leads.html` around lines 510 - 517, The `openImportErrors` function
has a type mismatch where `jobId` parameter is a string (from
`btn.dataset.jobId`) but `item.id` from the API is a number, causing the strict
equality check in the find method to always fail silently. Convert `jobId` to a
number using `parseInt` or the unary plus operator before comparing it with
`item.id` in the find condition to ensure the types match and the job lookup
succeeds.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LO-062 [Medium - Testing]: Add Integration Tests for Campaign Launch Flow

2 participants