Skip to content

[tests] Add WebSocket consumer tests #677#678

Open
shivsubh wants to merge 1 commit intoopenwisp:masterfrom
shivsubh:issues/677-issue-increase-test-coverage
Open

[tests] Add WebSocket consumer tests #677#678
shivsubh wants to merge 1 commit intoopenwisp:masterfrom
shivsubh:issues/677-issue-increase-test-coverage

Conversation

@shivsubh
Copy link

@shivsubh shivsubh commented Feb 5, 2026

Implement tests for RadiusBatchConsumer to verify authentication, authorization, and group messaging logic.
Increases coverage for consumers.py to 84%.

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Reference to Existing Issue

Closes #677

@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

Walkthrough

Adds a comprehensive end-to-end test suite for RadiusBatchConsumer at openwisp_radius/tests/test_consumers.py. The new TestRadiusBatchConsumer configures ASGI routing for /ws/radius/batch/{batch_id}/ and verifies WebSocket connection and authorization across superusers, staff (with/without permissions), unauthenticated/non-staff users, and group-based access. It exercises batch status update messaging via the channel layer, disconnect cleanup, and the internal _user_can_access_batch access checks (including invalid UUID handling). Additionally, multiple unit tests were added under test_counters, test_sqlite_counters, test_radclient, and test_commands. consumers.py simplifies _user_can_access_batch for non-superusers to use a direct exists() query.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client (WebSocket)
    participant ASGI as ASGI Router (/ws/radius/batch/{batch_id}/)
    participant Consumer as RadiusBatchConsumer
    participant Auth as Auth System
    participant DB as Database (RadiusBatch)
    participant Channel as Channel Layer

    Client->>ASGI: Open WebSocket /ws/radius/batch/{batch_id}/
    ASGI->>Consumer: Instantiate consumer (scope, path)
    Consumer->>Auth: authenticate user (scope)
    Auth-->>Consumer: user (superuser/staff/group/anon)
    alt user is superuser
        Consumer->>DB: (optional) skip org check
        DB-->>Consumer: n/a
        Consumer-->>Client: accept connection
    else non-superuser
        Consumer->>DB: RadiusBatch.objects.filter(pk=batch_id, organization__in=user.organizations_managed).exists()
        DB-->>Consumer: True/False
        alt exists True
            Consumer-->>Client: accept connection
        else
            Consumer-->>Client: reject connection
        end
    end

    %% Status update flow
    Client->>Consumer: send batch.status.update payload
    Consumer->>Channel: group_send "batch_{id}" update payload
    Channel-->>Consumer: delivers update to group members
    Consumer-->>Client: forwards update payload over WebSocket

    %% Disconnect cleanup
    Client->>Consumer: close WebSocket
    Consumer->>Channel: group_discard "batch_{id}" (cleanup)
    Channel-->>Consumer: confirm discard
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding WebSocket consumer tests. It directly references the issue number and is appropriately concise.
Description check ✅ Passed The pull request description follows the template structure with all required sections completed: checklist items marked, issue reference provided, and a clear description of the changes made.
Linked Issues check ✅ Passed The PR adds comprehensive tests for RadiusBatchConsumer covering authentication, authorization, and WebSocket messaging, which directly addresses the test coverage improvement objective in issue #677.
Out of Scope Changes check ✅ Passed All changes are within scope: test additions for RadiusBatchConsumer, a minor refactor in consumers.py to support testing, and related counter tests. No unrelated functionality was introduced.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @.github/workflows/ci.yml:
- Around line 102-107: Add a permissions block to the job granting
pull-requests: write, give the Tests step an id (e.g., id: tests) so downstream
steps can reference its conclusion, and update the "Comment coverage" step
(name: Comment coverage) to pin the action to
MishaKav/pytest-coverage-comment@v1.2.0 and change its condition to run only
when tests succeed (use steps.tests.conclusion == 'success' instead of the
current condition); keep the pytest-coverage-path input as-is.

In `@openwisp_radius/tests/test_consumers.py`:
- Around line 47-52: Replace hardcoded password literals passed to
User.objects.create_user in tests (e.g., the "staff_user" and other instances
created in test_consumers.py) with a shared constant or helper function; define
a PASSWORD constant (or a get_test_password() helper) at top of the test module
and use that constant when calling create_user so Ruff S106 is satisfied and all
occurrences (around the create_user calls at the referenced spots) are updated
to use the constant/helper.
- Around line 65-66: The test unpacking of the tuple returned by
_create_test_data() currently assigns values to names that go unused and
triggers RUF059; update each unpack to either unpack only the needed values or
rename unused names with a leading underscore (e.g., change "org, user, batch =
self._create_test_data()" to "_org, user, _batch = self._create_test_data()" or
similar) in all affected occurrences that call self._create_test_data()
(including the instances around lines 65, 82, 100, 118, 135, and 158) so unused
variables are prefixed with _ or omitted entirely.
- Around line 134-153: The test test_websocket_reject_no_permission fails
because RadiusBatchConsumer._user_can_access_batch currently discards the
queryset.exists() result and ends up returning True for non-superusers; update
the method in openwisp_radius/consumers.py
(RadiusBatchConsumer._user_can_access_batch) to return the actual boolean from
the permission check (e.g., return batch_qs.exists() or the equivalent
membership check) for non-superusers instead of always True, so staff users
without access are denied and the test expectation holds.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2646a5c and 0b0decd.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • openwisp_radius/tests/test_consumers.py
🧰 Additional context used
🧬 Code graph analysis (1)
openwisp_radius/tests/test_consumers.py (4)
openwisp_radius/consumers.py (3)
  • RadiusBatchConsumer (8-47)
  • connect (23-41)
  • disconnect (43-44)
openwisp_radius/utils.py (1)
  • load_model (33-34)
openwisp_radius/tests/__init__.py (2)
  • CreateRadiusObjectsMixin (53-150)
  • _create_radius_batch (137-142)
openwisp_radius/api/serializers.py (1)
  • user (673-674)
🪛 Ruff (0.14.14)
openwisp_radius/tests/test_consumers.py

[error] 50-50: Possible hardcoded password assigned to argument: "password"

(S106)


[error] 60-60: Possible hardcoded password assigned to argument: "password"

(S106)


[warning] 65-65: Unpacked variable org is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 82-82: Unpacked variable admin_user is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 100-100: Unpacked variable org is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 100-100: Unpacked variable user is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 118-118: Unpacked variable org is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 118-118: Unpacked variable admin_user is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 135-135: Unpacked variable org is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 135-135: Unpacked variable admin_user is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[error] 140-140: Possible hardcoded password assigned to argument: "password"

(S106)


[warning] 158-158: Unpacked variable org is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@shivsubh shivsubh force-pushed the issues/677-issue-increase-test-coverage branch 3 times, most recently from a4ac1b9 to d3f4f2c Compare February 5, 2026 10:44
Comment on lines -15 to -21
try:
RadiusBatch.objects.filter(
pk=batch_id, organization__in=user.organizations_managed
).exists()
return True
except ObjectDoesNotExist:
return False
Copy link
Author

Choose a reason for hiding this comment

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

This seems like a bug, because it will never raise ObjectDoesNotExist

Copy link
Member

Choose a reason for hiding this comment

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

Good catch, I think you're right.

@shivsubh shivsubh force-pushed the issues/677-issue-increase-test-coverage branch from d3f4f2c to 0942759 Compare February 5, 2026 10:46
Comment on lines 106 to 110
- name: Comment coverage
if: ${{ success() }}
uses: MishaKav/pytest-coverage-comment@v1.2.0
with:
pytest-coverage-path: coverage.xml
Copy link
Author

Choose a reason for hiding this comment

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

I added this so that we know about coverage in the PR. Let me know if i need to remove this.

Copy link
Member

Choose a reason for hiding this comment

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

Coveralls already takes care of this, so I think this is not needed.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, have removed this

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 5, 2026
@coveralls
Copy link

coveralls commented Feb 5, 2026

Coverage Status

coverage: 88.747% (-3.2%) from 91.995%
when pulling 57d42a6 on shivsubh:issues/677-issue-increase-test-coverage
into 33bbd55 on openwisp:master.

Copy link
Member

@nemesifier nemesifier left a comment

Choose a reason for hiding this comment

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

Thank you for this!

Covearlls has reported an increase to 88.45% (+0.3%) from 88.152%, pretty negligible. Was this your aim?

Comment on lines 106 to 110
- name: Comment coverage
if: ${{ success() }}
uses: MishaKav/pytest-coverage-comment@v1.2.0
with:
pytest-coverage-path: coverage.xml
Copy link
Member

Choose a reason for hiding this comment

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

Coveralls already takes care of this, so I think this is not needed.

)
return org, user, batch

def _create_staff_user(self, org):
Copy link
Member

Choose a reason for hiding this comment

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

Let's avoid duplicating test logic as much as possible, try reusing:
https://github.com/openwisp/openwisp-users/blob/master/openwisp_users/tests/utils.py#L32-L170

org.add_user(user, is_admin=True)
return user

def _create_regular_user(self):
Copy link
Member

Choose a reason for hiding this comment

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

same here

Comment on lines -15 to -21
try:
RadiusBatch.objects.filter(
pk=batch_id, organization__in=user.organizations_managed
).exists()
return True
except ObjectDoesNotExist:
return False
Copy link
Member

Choose a reason for hiding this comment

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

Good catch, I think you're right.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@openwisp_radius/tests/test_consumers.py`:
- Around line 159-174: test_websocket_group_connection only checks
connect/disconnect and must be updated to assert group membership and message
reception: inside the async test in test_websocket_group_connection, after
establishing connection with WebsocketCommunicator (communicator) and setting
communicator.scope user/url_route, get the channel layer (get_channel_layer()),
call group_send to "radius_batch_{batch.pk}" with a dict using the JSON handler
type (e.g., type that your consumer expects for JSON payloads) and a sample
payload, then await communicator.receive_json_from() and assert the received
payload equals the sent data; finally disconnect the communicator—this ensures
the consumer was added to the group and can receive group messages.
🧹 Nitpick comments (2)
openwisp_radius/tests/test_consumers.py (2)

109-109: Move AnonymousUser import to the top of the file.

This import is buried inside a nested async function. It should be at the module level with the other Django imports for consistency and readability.

Suggested fix

At top of file (e.g., after line 4):

 from django.contrib.auth import get_user_model
+from django.contrib.auth.models import AnonymousUser
 from django.test import TransactionTestCase

Then remove line 109 and use AnonymousUser directly in the test body.


77-78: Prefer self.assertTrue / self.assertFalse over bare assert.

Bare assert statements are silently skipped when Python runs with -O (optimized mode) and produce less informative failure messages than Django/unittest assertion methods. Since you already inherit from TransactionTestCase, use its assertion methods for consistency and better diagnostics.

For example:

-            connected, _ = await communicator.connect()
-            assert connected is True
+            connected, _ = await communicator.connect()
+            self.assertTrue(connected)
-            connected, _ = await communicator.connect()
-            assert connected is False
+            connected, _ = await communicator.connect()
+            self.assertFalse(connected)

Also applies to: 95-96, 114-115, 131-132, 154-155, 170-171

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c13c9db and f47b82b.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • openwisp_radius/consumers.py
  • openwisp_radius/tests/test_consumers.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/workflows/ci.yml
  • openwisp_radius/consumers.py
🧰 Additional context used
🧬 Code graph analysis (1)
openwisp_radius/tests/test_consumers.py (2)
openwisp_radius/consumers.py (3)
  • RadiusBatchConsumer (7-42)
  • connect (18-36)
  • disconnect (38-39)
openwisp_radius/utils.py (1)
  • load_model (33-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
🔇 Additional comments (4)
openwisp_radius/tests/test_consumers.py (4)

1-24: Imports and module-level setup look good.

The URLRouter, model loading, and test application setup are all correctly structured for WebSocket consumer testing.


27-64: Helper methods and class setup are well-organized.

Clean separation of concerns between _create_test_data, _create_staff_user, and _create_regular_user. TransactionTestCase is the correct base for async/channels tests, and the TEST_PASSWORD constant addresses the earlier S106 feedback.


66-81: Positive-path tests cover both superuser and org-admin correctly — LGTM.

The scope setup, connection assertion, and disconnect lifecycle are all handled properly. Good coverage of the two distinct access paths in _user_can_access_batch.

Also applies to: 83-99


101-134: Rejection tests cover all three deny paths — LGTM.

Unauthenticated, non-staff, and staff-without-permission scenarios correctly map to the three guard clauses in RadiusBatchConsumer.connect().

Also applies to: 136-157

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@shivsubh shivsubh force-pushed the issues/677-issue-increase-test-coverage branch 6 times, most recently from 36035d0 to 195124a Compare February 6, 2026 11:55
@shivsubh
Copy link
Author

shivsubh commented Feb 6, 2026

Thank you for this!

Covearlls has reported an increase to 88.45% (+0.3%) from 88.152%, pretty negligible. Was this your aim?

No, in my local it showed 95%, I am working on adding more tests.

@shivsubh shivsubh force-pushed the issues/677-issue-increase-test-coverage branch from 195124a to a4af870 Compare February 6, 2026 13:03
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@openwisp_radius/tests/test_radclient.py`:
- Around line 131-136: The test test_coa_packet_encode_key_values_empty
duplicates test_coa_packet by calling
CoaPacket._EncodeKeyValues("Session-Timeout", ""); remove the duplicate or
change it to assert a different scenario: update
test_coa_packet_encode_key_values_empty to call CoaPacket._EncodeKeyValues with
a distinct key or a non-empty/edge-case value (e.g., a string with whitespace, a
numeric string, or a different attribute name) and assert the expected tuple,
ensuring you reference the CoaPacket class and its _EncodeKeyValues method to
keep the test meaningfully distinct from test_coa_packet.
- Around line 126-129: Remove the duplicated assertion call to
mocked_logger.assert_called_with in the test — locate the two identical calls
that assert f"DisconnectNAK received from {client.client.server} for payload:
{attrs}" (the second is a copy-paste) and delete the redundant one so the test
only asserts this call once; keep the single assertion referencing
mocked_logger, client.client.server and attrs.
🧹 Nitpick comments (2)
openwisp_radius/tests/test_counters/test_base_counter.py (1)

141-151: Move imports to the top of the file.

MagicMock should be imported alongside patch on line 2 rather than inline. The BaseException import from counters.exceptions shadows Python's built-in BaseException — while it's scoped to this method, moving it to the top-level imports (and/or aliasing it, e.g., as CounterBaseException) would be clearer and consistent with how SkipCheck is imported on line 10.

Proposed fix

At the top of the file (lines 2, 10):

-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
 from ...counters.exceptions import SkipCheck
+from ...counters.exceptions import BaseException as CounterBaseException

Then in the test:

     def test_base_exception_logging(self):
-        from unittest.mock import MagicMock
-
-        from ...counters.exceptions import BaseException
-
         logger = MagicMock()
-        BaseException("message", "error", logger)
+        CounterBaseException("message", "error", logger)
         logger.error.assert_called_with("message")
         with self.assertRaises(AssertionError):
-            BaseException("message", "invalid_level", logger)
+            CounterBaseException("message", "invalid_level", logger)
openwisp_radius/tests/test_radclient.py (1)

138-142: Use more specific assertion methods.

assertGreaterEqual and assertIn produce more informative failure messages than assertTrue with manual comparisons.

Proposed fix
     def test_get_dictionaries(self):
         client = self._get_client()
         dicts = client.get_dictionaries()
-        self.assertTrue(len(dicts) >= 1)
-        self.assertTrue("dictionary" in dicts[0])
+        self.assertGreaterEqual(len(dicts), 1)
+        self.assertIn("dictionary", dicts[0])
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 195124a and a4af870.

📒 Files selected for processing (5)
  • openwisp_radius/consumers.py
  • openwisp_radius/tests/test_consumers.py
  • openwisp_radius/tests/test_counters/test_base_counter.py
  • openwisp_radius/tests/test_counters/test_sqlite_counters.py
  • openwisp_radius/tests/test_radclient.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • openwisp_radius/tests/test_consumers.py
🧰 Additional context used
🧬 Code graph analysis (2)
openwisp_radius/tests/test_counters/test_base_counter.py (1)
openwisp_radius/counters/exceptions.py (1)
  • BaseException (1-10)
openwisp_radius/tests/test_counters/test_sqlite_counters.py (3)
openwisp_radius/tests/test_counters/utils.py (1)
  • _get_kwargs (27-31)
openwisp_radius/counters/sqlite/daily_counter.py (1)
  • DailyCounter (5-13)
openwisp_radius/counters/sqlite/daily_traffic_counter.py (1)
  • DailyTrafficCounter (5-8)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.1.0
🔇 Additional comments (3)
openwisp_radius/consumers.py (1)

14-16: Clean simplification of the access check.

Replacing the try/except ObjectDoesNotExist with filter().exists() is a good improvement — it's more idiomatic, avoids exception-driven control flow, and is consistent with the superuser branch on line 12.

openwisp_radius/tests/test_counters/test_base_counter.py (1)

117-122: Good addition for edge-case coverage.

This subTest correctly validates the monthly subscription reset when the subscription day (4th) hasn't been reached yet in the current month (frozen at Nov 3rd), confirming the period falls in the previous cycle.

openwisp_radius/tests/test_counters/test_sqlite_counters.py (1)

127-138: Reasonable coverage additions for SQLite counter internals.

Both tests are straightforward: test_sqlite_organization_id_format validates UUID hyphen-stripping for SQLite compatibility, and test_sqlite_traffic_sql confirms the expected SQL fragment in the traffic counter. LGTM.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +126 to +129
mocked_logger.assert_called_with(
f"DisconnectNAK received from {client.client.server} "
f"for payload: {attrs}"
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove duplicated assertion — copy-paste error.

Lines 126-129 are an exact duplicate of lines 122-125. assert_called_with checks the most recent call, so invoking it twice with the same arguments adds no value and is clearly accidental.

Proposed fix
             mocked_logger.assert_called_with(
                 f"DisconnectNAK received from {client.client.server} "
                 f"for payload: {attrs}"
             )
-            mocked_logger.assert_called_with(
-                f"DisconnectNAK received from {client.client.server} "
-                f"for payload: {attrs}"
-            )
📝 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
mocked_logger.assert_called_with(
f"DisconnectNAK received from {client.client.server} "
f"for payload: {attrs}"
)
mocked_logger.assert_called_with(
f"DisconnectNAK received from {client.client.server} "
f"for payload: {attrs}"
)
🤖 Prompt for AI Agents
In `@openwisp_radius/tests/test_radclient.py` around lines 126 - 129, Remove the
duplicated assertion call to mocked_logger.assert_called_with in the test —
locate the two identical calls that assert f"DisconnectNAK received from
{client.client.server} for payload: {attrs}" (the second is a copy-paste) and
delete the redundant one so the test only asserts this call once; keep the
single assertion referencing mocked_logger, client.client.server and attrs.

Comment on lines +131 to +136
def test_coa_packet_encode_key_values_empty(self):
client = self._get_client()
packet = CoaPacket(dict=client.client.dict)
key = "Session-Timeout"
res = packet._EncodeKeyValues(key, "")
self.assertEqual(res, (key, ""))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate of existing test_coa_packet — remove or differentiate.

This test is functionally identical to lines 57-58 in test_coa_packet, which already asserts that _EncodeKeyValues("Session-Timeout", "") returns ("Session-Timeout", ""). Either remove this test or have it cover a genuinely different scenario (e.g., a different attribute key or a non-empty edge case not already covered).

🤖 Prompt for AI Agents
In `@openwisp_radius/tests/test_radclient.py` around lines 131 - 136, The test
test_coa_packet_encode_key_values_empty duplicates test_coa_packet by calling
CoaPacket._EncodeKeyValues("Session-Timeout", ""); remove the duplicate or
change it to assert a different scenario: update
test_coa_packet_encode_key_values_empty to call CoaPacket._EncodeKeyValues with
a distinct key or a non-empty/edge-case value (e.g., a string with whitespace, a
numeric string, or a different attribute name) and assert the expected tuple,
ensuring you reference the CoaPacket class and its _EncodeKeyValues method to
keep the test meaningfully distinct from test_coa_packet.

@shivsubh shivsubh force-pushed the issues/677-issue-increase-test-coverage branch from a4af870 to 0ac92df Compare February 6, 2026 13:17
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@openwisp_radius/tests/test_consumers.py`:
- Around line 151-182: The test_batch_status_update contains an orphaned
client-to-server send_json_to call that is dropped because RadiusBatchConsumer
does not implement receive_json; remove the communicator.send_json_to call in
test_batch_status_update (or alternatively implement a receive_json method on
RadiusBatchConsumer to handle the incoming
{"type":"batch.status.update","status":"completed"} messages) so the test only
exercises server-to-client group messages; update the test to rely solely on
channel_layer.group_send and communicator.receive_json_from if you choose
removal.
- Around line 44-59: Replace bare Python assert statements in the tests with
unittest assertion methods so failures integrate with Django's test runner:
change occurrences like "assert connected is True" to
"self.assertTrue(connected)" and "assert connected is False" to
"self.assertFalse(connected)" across the test methods (e.g.,
test_websocket_connect_superuser and the other test_* functions referenced),
ensuring you use self.assertTrue/self.assertFalse in their async inner functions
before disconnecting the WebsocketCommunicator.
- Around line 184-209: The test_disconnect_cleanup currently disconnects the
WebsocketCommunicator but never asserts the client stopped receiving messages;
after sending the group message with channel_layer.group_send you should assert
the disconnected communicator receives nothing. Modify the
test_disconnect_cleanup async block (using the existing communicator variable)
to await communicator.receive_nothing(timeout=0.1) or otherwise assert that no
message is returned (e.g., self.assertTrue(await
communicator.receive_nothing(...))) after the group_send call so the test
verifies the client was removed from the group.
🧹 Nitpick comments (4)
openwisp_radius/tests/test_counters/test_base_counter.py (2)

117-122: Redundant with existing "monthly_subscription prev month" subTest.

With freeze_time("2021-11-03") (day 3) and date_joined day 4, this exercises the same "join day > current day → previous month window" branch already covered by the "prev month" subTest (day 22 > day 3). The subTest label "future start date logic" is also a bit misleading since it's really testing the same previous-month rollback path.

If the intent is to cover a distinct edge case (e.g., join day = current day + 1), consider naming it accordingly or removing it if coverage is already sufficient.


141-151: Test logic is correct; move imports to the top of the file.

The test properly validates both the happy path (valid level triggers logging) and the error path (invalid level raises AssertionError), matching the BaseException.__init__ contract in counters/exceptions.py.

Minor nit: MagicMock is imported inside the method body (line 142) while patch from the same unittest.mock module is already imported at file level (line 2). Similarly, the BaseException import could be colocated with other counter imports near the top. Moving these to the top-level imports would be more consistent with the rest of the file.

openwisp_radius/consumers.py (1)

38-39: disconnect calls group_discard even when connect was rejected early.

When connect() closes the socket before group_add (lines 24 or 32), disconnect() still calls group_discard. This is safe with the in-memory channel layer (it's a no-op for non-member channels), but it's worth being aware of. If a stricter backend is ever used, consider guarding with an is_connected flag or catching potential errors.

openwisp_radius/tests/test_consumers.py (1)

81-97: Move imports to the top of the file.

AnonymousUser (line 89), get_channel_layer (lines 168, 200) are imported inside test method bodies. These are stable Django/Channels imports and should be at the module level for consistency and clarity.

Suggested fix
 from django.contrib.auth import get_user_model
+from django.contrib.auth.models import AnonymousUser
 from django.test import TransactionTestCase
 from django.urls import re_path
 
+from channels.layers import get_channel_layer
 from openwisp_users.tests.utils import TestOrganizationMixin
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4af870 and 0ac92df.

📒 Files selected for processing (5)
  • openwisp_radius/consumers.py
  • openwisp_radius/tests/test_consumers.py
  • openwisp_radius/tests/test_counters/test_base_counter.py
  • openwisp_radius/tests/test_counters/test_sqlite_counters.py
  • openwisp_radius/tests/test_radclient.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • openwisp_radius/tests/test_counters/test_sqlite_counters.py
  • openwisp_radius/tests/test_radclient.py
🧰 Additional context used
🧬 Code graph analysis (2)
openwisp_radius/consumers.py (1)
openwisp_radius/api/serializers.py (1)
  • user (698-699)
openwisp_radius/tests/test_counters/test_base_counter.py (1)
openwisp_radius/counters/exceptions.py (1)
  • BaseException (1-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
🔇 Additional comments (2)
openwisp_radius/consumers.py (1)

14-16: Good fix — exists() result is now correctly returned.

This properly fixes the previous bug where the non-superuser path always returned True because .exists() was called but its boolean result was discarded.

openwisp_radius/tests/test_consumers.py (1)

211-239: Good direct unit test of _user_can_access_batch.

This covers the three key access scenarios: superuser, staff with matching org, and staff with non-matching org. The assertions properly validate the exists()-based logic in the updated consumer.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +151 to +182
def test_batch_status_update(self):
_, user, batch = self._create_test_data()

async def test():
communicator = WebsocketCommunicator(
application,
f"/ws/radius/batch/{batch.pk}/",
)
communicator.scope["user"] = user
communicator.scope["url_route"] = {"kwargs": {"batch_id": str(batch.pk)}}

connected, _ = await communicator.connect()
assert connected is True
await communicator.send_json_to(
{"type": "batch.status.update", "status": "completed"}
)

from channels.layers import get_channel_layer

channel_layer = get_channel_layer()

await channel_layer.group_send(
f"radius_batch_{batch.pk}",
{"type": "batch_status_update", "status": "processing"},
)

response = await communicator.receive_json_from()
assert response == {"status": "processing"}

await communicator.disconnect()

async_to_sync(test)()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Orphaned send_json_to on lines 164-166 serves no purpose.

RadiusBatchConsumer does not override receive_json, so the send_json_to call on line 164 is silently dropped by the default AsyncJsonWebsocketConsumer. It doesn't test anything and may mislead future readers into thinking client-to-server messages are part of the protocol. Remove it or add a receive_json handler to the consumer if client-to-server messages are intended.

Suggested fix
             connected, _ = await communicator.connect()
-            assert connected is True
-            await communicator.send_json_to(
-                {"type": "batch.status.update", "status": "completed"}
-            )
+            self.assertTrue(connected)

             from channels.layers import get_channel_layer
🤖 Prompt for AI Agents
In `@openwisp_radius/tests/test_consumers.py` around lines 151 - 182, The
test_batch_status_update contains an orphaned client-to-server send_json_to call
that is dropped because RadiusBatchConsumer does not implement receive_json;
remove the communicator.send_json_to call in test_batch_status_update (or
alternatively implement a receive_json method on RadiusBatchConsumer to handle
the incoming {"type":"batch.status.update","status":"completed"} messages) so
the test only exercises server-to-client group messages; update the test to rely
solely on channel_layer.group_send and communicator.receive_json_from if you
choose removal.

Comment on lines +184 to +209
def test_disconnect_cleanup(self):
_, user, batch = self._create_test_data()

async def test():
communicator = WebsocketCommunicator(
application,
f"/ws/radius/batch/{batch.pk}/",
)
communicator.scope["user"] = user
communicator.scope["url_route"] = {"kwargs": {"batch_id": str(batch.pk)}}

connected, _ = await communicator.connect()
assert connected is True

await communicator.disconnect()

from channels.layers import get_channel_layer

channel_layer = get_channel_layer()

await channel_layer.group_send(
f"radius_batch_{batch.pk}",
{"type": "batch_status_update", "status": "completed"},
)

async_to_sync(test)()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

test_disconnect_cleanup doesn't actually verify that the disconnected client stops receiving messages.

After disconnecting and sending a group message, the test ends without asserting anything. It only proves group_send doesn't raise an exception—it doesn't verify the client was actually removed from the group. To make this meaningful, assert that no message is received (e.g., using a timeout):

Suggested improvement
             await channel_layer.group_send(
                 f"radius_batch_{batch.pk}",
                 {"type": "batch_status_update", "status": "completed"},
             )
+
+            import asyncio
+
+            with self.assertRaises(asyncio.TimeoutError):
+                await asyncio.wait_for(
+                    communicator.receive_json_from(), timeout=0.1
+                )

Alternatively, you can use communicator.receive_nothing() if supported by your channels testing version:

self.assertTrue(await communicator.receive_nothing(timeout=0.1))
🤖 Prompt for AI Agents
In `@openwisp_radius/tests/test_consumers.py` around lines 184 - 209, The
test_disconnect_cleanup currently disconnects the WebsocketCommunicator but
never asserts the client stopped receiving messages; after sending the group
message with channel_layer.group_send you should assert the disconnected
communicator receives nothing. Modify the test_disconnect_cleanup async block
(using the existing communicator variable) to await
communicator.receive_nothing(timeout=0.1) or otherwise assert that no message is
returned (e.g., self.assertTrue(await communicator.receive_nothing(...))) after
the group_send call so the test verifies the client was removed from the group.

@shivsubh shivsubh force-pushed the issues/677-issue-increase-test-coverage branch from 0ac92df to dc75288 Compare February 6, 2026 14:09
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@openwisp_radius/tests/test_counters/test_resets.py`:
- Around line 94-104: The test test_monthly_subscription_with_kwargs asserts
wrong expected timestamps: when freeze_time("2021-11-30T23:59:59") and
user.date_joined day is 15, _monthly_subscription(user, counter=None) should
produce start = 2021-11-15 and end = 2021-12-15, so update the assertions in
test_monthly_subscription_with_kwargs to expect "2021-11-15 00:00:00" for start
and "2021-12-15 00:00:00" for end (or adjust freeze_time/date_joined to match
the original expected month if you prefer changing inputs instead of expected
values).
- Around line 80-82: The second assertion duplicates the first by checking
`start` again; change the middle assertion to assert `end` (e.g., replace the
second self.assertEqual(str(datetime.fromtimestamp(start)), "2021-10-22
00:00:00") with self.assertEqual(str(datetime.fromtimestamp(end)), "2021-11-22
00:00:00") in the test (so the three asserts validate start and end correctly),
and optionally remove any exact duplicate assertion afterward to avoid
redundancy.
🧹 Nitpick comments (4)
openwisp_radius/tests/test_counters/test_exceptions.py (1)

1-7: Unused imports: datetime and freeze_time.

datetime (line 1) and freeze_time (line 4) are imported but never used in this file. They appear to be copy-paste artifacts.

♻️ Proposed fix
-from datetime import datetime
 from unittest.mock import MagicMock
 
-from freezegun import freeze_time
-
 from ...counters.exceptions import BaseException, MaxQuotaReached, SkipCheck
 from ..mixins import BaseTestCase
openwisp_radius/tests/test_commands.py (1)

568-590: Wrapper tests are straightforward; assertIsNotNone is redundant.

The assertIsNotNone(command) assertions on lines 572 and 583 add no value — if Command() failed to instantiate, the test would already fail with an exception before reaching them. Consider removing those lines to keep the tests concise. Otherwise, the inheritance checks are correct.

openwisp_radius/tests/test_counters/test_resets.py (1)

106-111: Duplicate assertion on line 110.

self.assertEqual(start, 0) on line 110 is identical to line 108. Consider replacing it with a different meaningful assertion or removing it.

openwisp_radius/tests/test_consumers.py (1)

89-91: Move AnonymousUser import to the top of the file.

Importing AnonymousUser inside the nested async def test() is functional but unconventional. Top-level imports improve readability and avoid repeated import overhead if the test is called in a loop.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ac92df and dc75288.

📒 Files selected for processing (8)
  • openwisp_radius/consumers.py
  • openwisp_radius/tests/test_commands.py
  • openwisp_radius/tests/test_consumers.py
  • openwisp_radius/tests/test_counters/test_base_counter.py
  • openwisp_radius/tests/test_counters/test_exceptions.py
  • openwisp_radius/tests/test_counters/test_resets.py
  • openwisp_radius/tests/test_counters/test_sqlite_counters.py
  • openwisp_radius/tests/test_radclient.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • openwisp_radius/tests/test_counters/test_base_counter.py
  • openwisp_radius/tests/test_radclient.py
  • openwisp_radius/tests/test_counters/test_sqlite_counters.py
🧰 Additional context used
🧬 Code graph analysis (4)
openwisp_radius/tests/test_counters/test_exceptions.py (2)
openwisp_radius/counters/exceptions.py (3)
  • BaseException (1-10)
  • MaxQuotaReached (21-28)
  • SkipCheck (13-18)
openwisp_radius/tests/mixins.py (1)
  • BaseTestCase (228-231)
openwisp_radius/tests/test_consumers.py (5)
openwisp_radius/consumers.py (4)
  • RadiusBatchConsumer (7-42)
  • connect (18-36)
  • disconnect (38-39)
  • _user_can_access_batch (8-16)
openwisp_radius/utils.py (1)
  • load_model (33-34)
openwisp_radius/tests/__init__.py (2)
  • CreateRadiusObjectsMixin (53-150)
  • _create_radius_batch (137-142)
tests/openwisp2/sample_users/models.py (1)
  • User (20-29)
openwisp_radius/api/serializers.py (1)
  • user (698-699)
openwisp_radius/tests/test_counters/test_resets.py (2)
openwisp_radius/counters/resets.py (7)
  • _daily (14-18)
  • _monthly (29-33)
  • _monthly_subscription (36-45)
  • _never (48-49)
  • _timestamp (10-11)
  • _today (6-7)
  • _weekly (21-26)
openwisp_radius/tests/mixins.py (1)
  • BaseTestCase (228-231)
openwisp_radius/tests/test_commands.py (4)
openwisp_radius/management/commands/convert_called_station_id.py (1)
  • Command (4-5)
openwisp_radius/management/commands/base/convert_called_station_id.py (1)
  • BaseConvertCalledStationIdCommand (22-164)
openwisp_radius/management/commands/base/__init__.py (1)
  • BatchAddMixin (8-24)
openwisp_radius/management/commands/base/prefix_add_users.py (1)
  • BasePrefixAddUsersCommand (12-71)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
🔇 Additional comments (3)
openwisp_radius/tests/test_counters/test_exceptions.py (1)

10-83: Tests are straightforward and provide good coverage for the exception classes.

The level-specific tests (debug, info, warn, error, critical, exception), the invalid-level test, and the subclass tests for SkipCheck and MaxQuotaReached are all correct and aligned with the source in openwisp_radius/counters/exceptions.py.

openwisp_radius/consumers.py (1)

14-16: Good fix — the exists() result is now properly returned.

The previous implementation discarded the .exists() boolean and always returned True for non-superusers. This correctly returns the query result, aligning the non-superuser path with the superuser path's pattern.

openwisp_radius/tests/test_consumers.py (1)

211-263: Direct _user_can_access_batch tests and invalid UUID tests look good.

These three tests (test_user_can_access_batch_method, test_invalid_batch_id, test_user_can_access_batch_with_invalid_uuid) meaningfully exercise the access-control logic — including cross-org denial and non-existent batch handling. They correctly validate the exists()-based fix in consumers.py.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +80 to +89
self.assertEqual(str(datetime.fromtimestamp(start)), "2021-10-22 00:00:00")
self.assertEqual(str(datetime.fromtimestamp(start)), "2021-10-22 00:00:00")
self.assertEqual(str(datetime.fromtimestamp(end)), "2021-11-22 00:00:00")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Line 81 is a duplicate of line 80 — likely meant to assert end.

Both lines 80 and 81 assert the same thing (start). Line 81 should verify end instead.

🐛 Proposed fix
         self.assertEqual(str(datetime.fromtimestamp(start)), "2021-10-22 00:00:00")
-        self.assertEqual(str(datetime.fromtimestamp(start)), "2021-10-22 00:00:00")
+        self.assertEqual(str(datetime.fromtimestamp(end)), "2021-11-22 00:00:00")
         self.assertEqual(str(datetime.fromtimestamp(end)), "2021-11-22 00:00:00")

Note: after fixing, lines 81 and 82 will be identical, so you may want to remove one of them too.

🤖 Prompt for AI Agents
In `@openwisp_radius/tests/test_counters/test_resets.py` around lines 80 - 82, The
second assertion duplicates the first by checking `start` again; change the
middle assertion to assert `end` (e.g., replace the second
self.assertEqual(str(datetime.fromtimestamp(start)), "2021-10-22 00:00:00") with
self.assertEqual(str(datetime.fromtimestamp(end)), "2021-11-22 00:00:00") in the
test (so the three asserts validate start and end correctly), and optionally
remove any exact duplicate assertion afterward to avoid redundancy.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@openwisp_radius/tests/test_radclient.py`:
- Around line 156-160: The test named test_rad_client_repr is misleading because
it never exercises repr/str; update it to either (A) rename the test to
test_rad_client_initialization and keep the existing assertions to reflect it
verifies _get_client wiring (checking client.client.server and
client.client.secret), or (B) modify the test body to assert the textual
representation by calling repr(client) or str(client) and checking the expected
string; locate the test function test_rad_client_repr and update its name or
body accordingly to match the intended behavior.
- Around line 151-154: The test test_disconnect_packet_default_code duplicates
test_disconnect_packet and provides no new coverage; either remove
test_disconnect_packet_default_code entirely, or modify it to assert different
behavior (for example, pass an explicit code to DisconnectPacket and assert
packet.code equals that explicit value) — locate the duplicate by the test name
test_disconnect_packet_default_code and the symbols DisconnectPacket and
DisconnectRequest and either delete the redundant test or change its
setup/assertion to test explicit-code handling.
🧹 Nitpick comments (6)
openwisp_radius/tests/test_radclient.py (2)

138-142: Prefer assertGreaterEqual / assertIn for better failure messages.

assertTrue(len(dicts) >= 1) and assertTrue("dictionary" in dicts[0]) produce unhelpful AssertionError: False is not true on failure. The dedicated assertions give context automatically.

Proposed fix
-        self.assertTrue(len(dicts) >= 1)
-        self.assertTrue("dictionary" in dicts[0])
+        self.assertGreaterEqual(len(dicts), 1)
+        self.assertIn("dictionary", dicts[0])

144-149: Remove redundant local import of DisconnectNAK.

DisconnectNAK is already imported at file scope (line 5). The local re-import on line 145 is unnecessary.

Proposed fix
     def test_disconnect_packet_with_custom_code(self):
-        from pyrad.packet import DisconnectNAK
-
         client = self._get_client()
         packet = DisconnectPacket(code=DisconnectNAK, dict=client.client.dict)
         self.assertEqual(packet.code, DisconnectNAK)
openwisp_radius/tests/test_counters/test_sqlite_counters.py (2)

126-145: test_sqlite_counter_mixin_init is essentially a duplicate of test_sqlite_organization_id_format.

Both tests create a DailyCounter with "Max-Daily-Session" kwargs, assert that counter.organization_id contains no hyphens, and assert it equals str(group.organization_id).replace("-", ""). The only difference is that test_sqlite_counter_mixin_init additionally asserts the original UUID contains hyphens, which is a tautological check on Python's str(uuid) format.

Consider removing one of these or merging them into a single, more focused test.


147-156: Test asserts static string content — low value and fragile.

test_sqlite_traffic_mixin_sql_property only checks substrings of a class-level string constant (SqliteTrafficMixin.sql). This doesn't exercise any runtime behavior; it will break if the SQL is reformatted, and it provides minimal coverage benefit. If the goal is to guard against accidental SQL changes, consider a snapshot/equality assertion instead.

openwisp_radius/tests/test_consumers.py (2)

27-29: TestOrganizationMixin is redundant in the base class list.

CreateRadiusObjectsMixin already inherits from TestOrganizationMixin (see openwisp_radius/tests/__init__.py, line 52), so listing it again in the MRO is unnecessary.

Suggested fix
-class TestRadiusBatchConsumer(
-    CreateRadiusObjectsMixin, TestOrganizationMixin, TransactionTestCase
-):
+class TestRadiusBatchConsumer(CreateRadiusObjectsMixin, TransactionTestCase):

89-89: Move inline imports to the top of the file.

AnonymousUser (line 89) and get_channel_layer (lines 168, 200) are imported inside test functions. These are stable imports with no side effects and should be at module level for clarity and consistency.

Suggested fix

At the top of the file, add:

from channels.layers import get_channel_layer
from django.contrib.auth.models import AnonymousUser

Then remove the corresponding inline imports from lines 89, 168, and 200.

Also applies to: 168-168, 200-200

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc75288 and e27f921.

📒 Files selected for processing (6)
  • openwisp_radius/tests/test_consumers.py
  • openwisp_radius/tests/test_counters/test_base_counter.py
  • openwisp_radius/tests/test_counters/test_exceptions.py
  • openwisp_radius/tests/test_counters/test_resets.py
  • openwisp_radius/tests/test_counters/test_sqlite_counters.py
  • openwisp_radius/tests/test_radclient.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • openwisp_radius/tests/test_counters/test_base_counter.py
  • openwisp_radius/tests/test_counters/test_exceptions.py
  • openwisp_radius/tests/test_counters/test_resets.py
🧰 Additional context used
🧬 Code graph analysis (3)
openwisp_radius/tests/test_counters/test_sqlite_counters.py (3)
openwisp_radius/counters/sqlite/daily_counter.py (1)
  • DailyCounter (5-13)
openwisp_radius/counters/base.py (1)
  • sql (44-45)
openwisp_radius/counters/sqlite/__init__.py (1)
  • SqliteTrafficMixin (8-15)
openwisp_radius/tests/test_radclient.py (1)
openwisp_radius/radclient/client.py (4)
  • CoaPacket (18-29)
  • _EncodeKeyValues (19-22)
  • get_dictionaries (45-46)
  • DisconnectPacket (32-34)
openwisp_radius/tests/test_consumers.py (3)
openwisp_radius/consumers.py (4)
  • RadiusBatchConsumer (7-42)
  • connect (18-36)
  • disconnect (38-39)
  • _user_can_access_batch (8-16)
openwisp_radius/utils.py (1)
  • load_model (33-34)
openwisp_radius/tests/__init__.py (2)
  • CreateRadiusObjectsMixin (53-150)
  • _create_radius_batch (137-142)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
🔇 Additional comments (4)
openwisp_radius/tests/test_consumers.py (4)

33-42: _create_test_data setup looks correct.

Creates an org, an admin (superuser), and a RadiusBatch associated with that org. Nicely structured for reuse across tests.


61-79: Good coverage of staff-with-permission path.

This test correctly verifies that a staff user who is an administrator for the batch's organization is allowed to connect, exercising the non-superuser branch of _user_can_access_batch.


81-132: Rejection tests for unauthenticated, non-staff, and no-permission users look correct.

Good coverage of the three rejection branches in RadiusBatchConsumer.connect. The test setups correctly isolate each condition.


211-265: Direct access-control tests and invalid batch ID tests are well structured.

These tests exercise _user_can_access_batch synchronously, covering the positive case (admin for batch org), cross-org denial, non-existent UUID, and the WebSocket-level rejection for an invalid batch ID. Good coverage.

One minor note: test_user_can_access_batch_with_invalid_uuid (lines 258–265) and test_invalid_batch_id (lines 241–256) both test the same logical scenario (non-existent batch UUID) — one via direct method call and one via WebSocket. This is intentional and fine for covering both code paths.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +156 to +160
def test_rad_client_repr(self):
client = self._get_client()
self.assertIsNotNone(client.client)
self.assertEqual(client.client.server, "127.0.0.1")
self.assertEqual(client.client.secret, b"testing")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Misleading test name — this doesn't test repr.

The method is named test_rad_client_repr but it never calls repr() or str() on any object. It only checks that the constructor wires up server and secret, which is already implicitly covered by every test that uses _get_client(). Either rename it to reflect what it actually tests (e.g., test_rad_client_initialization) or replace the body with an actual repr/str assertion if that's what was intended.

🤖 Prompt for AI Agents
In `@openwisp_radius/tests/test_radclient.py` around lines 156 - 160, The test
named test_rad_client_repr is misleading because it never exercises repr/str;
update it to either (A) rename the test to test_rad_client_initialization and
keep the existing assertions to reflect it verifies _get_client wiring (checking
client.client.server and client.client.secret), or (B) modify the test body to
assert the textual representation by calling repr(client) or str(client) and
checking the expected string; locate the test function test_rad_client_repr and
update its name or body accordingly to match the intended behavior.

@shivsubh shivsubh force-pushed the issues/677-issue-increase-test-coverage branch 2 times, most recently from 745fb70 to 1767dcc Compare February 6, 2026 14:30
@shivsubh shivsubh force-pushed the issues/677-issue-increase-test-coverage branch from 1767dcc to 8d88629 Compare February 6, 2026 14:38
Added new test modules: test_consumers.py, test_exceptions.py, and test_resets.py.
Fixed formatting issues (W293) in test_resets.py.
Updated consumers.py and existing tests to resolve failures and improve coverage.

Fixes openwisp#677
@shivsubh shivsubh force-pushed the issues/677-issue-increase-test-coverage branch from 8d88629 to 57d42a6 Compare February 6, 2026 14:51
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@openwisp_radius/tests/test_counters/test_resets.py`:
- Around line 113-118: In test_never_reset, remove the duplicate assertion on
start (the second self.assertEqual(start, 0)) and replace the deprecated
datetime.utcfromtimestamp usage with datetime.fromtimestamp(start,
tz=timezone.utc); update the test name reference (test_never_reset) and ensure
the datetime.timezone is imported or referenced so the call to
fromtimestamp(start, tz=timezone.utc) compiles correctly when validating the
1970 year for the _never() result.
🧹 Nitpick comments (6)
openwisp_radius/tests/test_commands.py (2)

568-577: Low-value test — only verifies Python inheritance, not command behavior.

This test instantiates Command and checks assertIsNotNone + assertIsInstance. Since Command is just class Command(BaseConvertCalledStationIdCommand): pass, this can never fail unless the import breaks. It doesn't exercise any logic in handle(), argument parsing, or helper methods, so it contributes no meaningful coverage.

Additionally, assertIsNotNone(command) on line 572 is redundant — a constructor returns an instance or raises; it never returns None.

Consider replacing this with a test that actually exercises the command's behavior (e.g., argument parsing, a mocked handle() call) to get real coverage improvement.


579-590: Same concern as above — assertIsNotNone is redundant and the test only checks inheritance.

Command() on line 582 will either succeed (returning a non-None instance) or raise, making line 583 dead weight. The assertIsInstance checks on lines 589-590 only confirm the MRO, which is already guaranteed by the class definition.

Note that test_prefix_add_users_command (line 229) already exercises the real prefix_add_users command end-to-end. This wrapper test adds no incremental coverage of meaningful code paths.

openwisp_radius/tests/test_counters/test_base_counter.py (2)

141-150: Move imports to the top of the file.

MagicMock and BaseException are imported inside the test method body. These should be at the module level for consistency and readability. Also note that importing BaseException from the counters package shadows Python's built-in BaseException — consider using an alias.

♻️ Suggested refactor

At the top of the file, add:

from unittest.mock import MagicMock
from ...counters.exceptions import BaseException as CounterBaseException

Then in the test:

     def test_base_exception_logging(self):
-        from unittest.mock import MagicMock
-
-        from ...counters.exceptions import BaseException
-
         logger = MagicMock()
-        BaseException("message", "error", logger)
+        CounterBaseException("message", "error", logger)
         logger.error.assert_called_with("message")
         with self.assertRaises(AssertionError):
-            BaseException("message", "invalid_level", logger)
+            CounterBaseException("message", "invalid_level", logger)

152-166: Move imports to the top of the file.

DailyCounter and _acct_data are imported inside the test method body. Move them to the module level for consistency with the rest of the file.

♻️ Suggested refactor

At the top of the file:

from ...counters.sqlite.daily_counter import DailyCounter
from .utils import _acct_data

Then simplify the test body:

     def test_consumed_method(self):
         opts = self._get_kwargs("Max-Daily-Session")
-        from ...counters.sqlite.daily_counter import DailyCounter
-
         counter = DailyCounter(**opts)
         consumed = counter.consumed()
         self.assertEqual(consumed, 0)
         self.assertIsInstance(consumed, int)
-
-        from .utils import _acct_data
-
         self._create_radius_accounting(**_acct_data)
         consumed = counter.consumed()
         self.assertEqual(consumed, int(_acct_data["session_time"]))
         self.assertIsInstance(consumed, int)
openwisp_radius/tests/test_consumers.py (2)

81-97: Move AnonymousUser import to the top of the file.

The import on line 89 is inside an async function. Move it to the module level alongside the other Django imports.

♻️ Suggested refactor

At the top of the file (after line 4):

 from django.contrib.auth import get_user_model
+from django.contrib.auth.models import AnonymousUser

Then remove line 89.


168-170: Move get_channel_layer import to the top of the file.

get_channel_layer is imported inside async test functions in both test_batch_status_update (line 168) and test_disconnect_cleanup (line 200). Move it to the module level.

♻️ Suggested refactor

At the top of the file:

+from channels.layers import get_channel_layer

Then remove lines 168 and 200.

Also applies to: 200-202

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e27f921 and 57d42a6.

📒 Files selected for processing (8)
  • openwisp_radius/consumers.py
  • openwisp_radius/tests/test_commands.py
  • openwisp_radius/tests/test_consumers.py
  • openwisp_radius/tests/test_counters/test_base_counter.py
  • openwisp_radius/tests/test_counters/test_exceptions.py
  • openwisp_radius/tests/test_counters/test_resets.py
  • openwisp_radius/tests/test_counters/test_sqlite_counters.py
  • openwisp_radius/tests/test_radclient.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • openwisp_radius/tests/test_counters/test_sqlite_counters.py
  • openwisp_radius/tests/test_radclient.py
  • openwisp_radius/tests/test_counters/test_exceptions.py
  • openwisp_radius/consumers.py
🧰 Additional context used
🧬 Code graph analysis (4)
openwisp_radius/tests/test_counters/test_base_counter.py (3)
openwisp_radius/counters/exceptions.py (1)
  • BaseException (1-10)
openwisp_radius/tests/test_counters/utils.py (1)
  • _get_kwargs (27-31)
openwisp_radius/counters/base.py (1)
  • consumed (147-148)
openwisp_radius/tests/test_consumers.py (2)
openwisp_radius/consumers.py (3)
  • connect (18-36)
  • disconnect (38-39)
  • _user_can_access_batch (8-16)
openwisp_radius/utils.py (1)
  • load_model (33-34)
openwisp_radius/tests/test_commands.py (4)
openwisp_radius/management/commands/convert_called_station_id.py (1)
  • Command (4-5)
openwisp_radius/management/commands/base/convert_called_station_id.py (1)
  • BaseConvertCalledStationIdCommand (22-164)
openwisp_radius/management/commands/base/__init__.py (1)
  • BatchAddMixin (8-24)
openwisp_radius/management/commands/base/prefix_add_users.py (1)
  • BasePrefixAddUsersCommand (12-71)
openwisp_radius/tests/test_counters/test_resets.py (1)
openwisp_radius/counters/resets.py (7)
  • _daily (14-18)
  • _monthly (29-33)
  • _monthly_subscription (36-45)
  • _never (48-49)
  • _timestamp (10-11)
  • _today (6-7)
  • _weekly (21-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
🔇 Additional comments (4)
openwisp_radius/tests/test_counters/test_base_counter.py (1)

117-122: LGTM!

The subTest correctly verifies the _monthly_subscription branch where day_joined=4 is greater than the current day (3rd), causing the start to shift to the previous month. Expected values align with the logic in resets.py.

openwisp_radius/tests/test_counters/test_resets.py (1)

101-111: Previous review concern about incorrect expected values has been addressed.

The expected values now correctly reflect that with freeze_time("2021-11-30T23:59:59") and day_joined=15, the start is Nov 15 (not in the future relative to Nov 30), yielding start="2021-11-15" and end="2021-12-15".

openwisp_radius/tests/test_consumers.py (2)

211-239: Good coverage of _user_can_access_batch across multiple authorization scenarios.

Tests superuser access, staff with correct org, and staff with wrong org. This directly validates the fixed consumer logic.


241-265: LGTM — good edge-case coverage for invalid batch IDs.

Both the WebSocket-level rejection and the direct method call with a non-existent UUID are tested, which is valuable for ensuring graceful handling of invalid input.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +113 to +118
def test_never_reset(self):
start, end = _never()
self.assertEqual(start, 0)
self.assertIsNone(end)
self.assertEqual(start, 0)
self.assertEqual(datetime.utcfromtimestamp(start).year, 1970)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate assertion and deprecated API.

  1. Line 117 (self.assertEqual(start, 0)) is an exact duplicate of line 115.
  2. Line 118 uses datetime.utcfromtimestamp, which is deprecated since Python 3.12. Use datetime.fromtimestamp(start, tz=timezone.utc) instead.
♻️ Proposed fix
+from datetime import date, datetime, timezone
+
     def test_never_reset(self):
         start, end = _never()
         self.assertEqual(start, 0)
         self.assertIsNone(end)
-        self.assertEqual(start, 0)
-        self.assertEqual(datetime.utcfromtimestamp(start).year, 1970)
+        self.assertEqual(datetime.fromtimestamp(start, tz=timezone.utc).year, 1970)
🤖 Prompt for AI Agents
In `@openwisp_radius/tests/test_counters/test_resets.py` around lines 113 - 118,
In test_never_reset, remove the duplicate assertion on start (the second
self.assertEqual(start, 0)) and replace the deprecated datetime.utcfromtimestamp
usage with datetime.fromtimestamp(start, tz=timezone.utc); update the test name
reference (test_never_reset) and ensure the datetime.timezone is imported or
referenced so the call to fromtimestamp(start, tz=timezone.utc) compiles
correctly when validating the 1970 year for the _never() result.

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.

[tests] Increase test coverage to 95%

3 participants