[tests] Add WebSocket consumer tests #677#678
[tests] Add WebSocket consumer tests #677#678shivsubh wants to merge 1 commit intoopenwisp:masterfrom
Conversation
WalkthroughAdds 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
.github/workflows/ci.ymlopenwisp_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.
a4ac1b9 to
d3f4f2c
Compare
| try: | ||
| RadiusBatch.objects.filter( | ||
| pk=batch_id, organization__in=user.organizations_managed | ||
| ).exists() | ||
| return True | ||
| except ObjectDoesNotExist: | ||
| return False |
There was a problem hiding this comment.
This seems like a bug, because it will never raise ObjectDoesNotExist
There was a problem hiding this comment.
Good catch, I think you're right.
d3f4f2c to
0942759
Compare
.github/workflows/ci.yml
Outdated
| - name: Comment coverage | ||
| if: ${{ success() }} | ||
| uses: MishaKav/pytest-coverage-comment@v1.2.0 | ||
| with: | ||
| pytest-coverage-path: coverage.xml |
There was a problem hiding this comment.
I added this so that we know about coverage in the PR. Let me know if i need to remove this.
There was a problem hiding this comment.
Coveralls already takes care of this, so I think this is not needed.
0942759 to
eec76ed
Compare
nemesifier
left a comment
There was a problem hiding this comment.
Thank you for this!
Covearlls has reported an increase to 88.45% (+0.3%) from 88.152%, pretty negligible. Was this your aim?
.github/workflows/ci.yml
Outdated
| - name: Comment coverage | ||
| if: ${{ success() }} | ||
| uses: MishaKav/pytest-coverage-comment@v1.2.0 | ||
| with: | ||
| pytest-coverage-path: coverage.xml |
There was a problem hiding this comment.
Coveralls already takes care of this, so I think this is not needed.
| ) | ||
| return org, user, batch | ||
|
|
||
| def _create_staff_user(self, org): |
There was a problem hiding this comment.
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): |
| try: | ||
| RadiusBatch.objects.filter( | ||
| pk=batch_id, organization__in=user.organizations_managed | ||
| ).exists() | ||
| return True | ||
| except ObjectDoesNotExist: | ||
| return False |
There was a problem hiding this comment.
Good catch, I think you're right.
There was a problem hiding this comment.
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: MoveAnonymousUserimport 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 TransactionTestCaseThen remove line 109 and use
AnonymousUserdirectly in the test body.
77-78: Preferself.assertTrue/self.assertFalseover bareassert.Bare
assertstatements 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 fromTransactionTestCase, 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
📒 Files selected for processing (3)
.github/workflows/ci.ymlopenwisp_radius/consumers.pyopenwisp_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.TransactionTestCaseis the correct base for async/channels tests, and theTEST_PASSWORDconstant 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.
36035d0 to
195124a
Compare
No, in my local it showed 95%, I am working on adding more tests. |
195124a to
a4af870
Compare
There was a problem hiding this comment.
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.
MagicMockshould be imported alongsidepatchon line 2 rather than inline. TheBaseExceptionimport fromcounters.exceptionsshadows Python's built-inBaseException— 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 howSkipCheckis 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, patchfrom ...counters.exceptions import SkipCheck +from ...counters.exceptions import BaseException as CounterBaseExceptionThen 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.
assertGreaterEqualandassertInproduce more informative failure messages thanassertTruewith 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
📒 Files selected for processing (5)
openwisp_radius/consumers.pyopenwisp_radius/tests/test_consumers.pyopenwisp_radius/tests/test_counters/test_base_counter.pyopenwisp_radius/tests/test_counters/test_sqlite_counters.pyopenwisp_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 ObjectDoesNotExistwithfilter().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_formatvalidates UUID hyphen-stripping for SQLite compatibility, andtest_sqlite_traffic_sqlconfirms 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.
| mocked_logger.assert_called_with( | ||
| f"DisconnectNAK received from {client.client.server} " | ||
| f"for payload: {attrs}" | ||
| ) |
There was a problem hiding this comment.
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.
| 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.
| 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, "")) |
There was a problem hiding this comment.
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.
a4af870 to
0ac92df
Compare
There was a problem hiding this comment.
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) anddate_joinedday 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 theBaseException.__init__contract incounters/exceptions.py.Minor nit:
MagicMockis imported inside the method body (line 142) whilepatchfrom the sameunittest.mockmodule is already imported at file level (line 2). Similarly, theBaseExceptionimport 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:disconnectcallsgroup_discardeven whenconnectwas rejected early.When
connect()closes the socket beforegroup_add(lines 24 or 32),disconnect()still callsgroup_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 anis_connectedflag 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
📒 Files selected for processing (5)
openwisp_radius/consumers.pyopenwisp_radius/tests/test_consumers.pyopenwisp_radius/tests/test_counters/test_base_counter.pyopenwisp_radius/tests/test_counters/test_sqlite_counters.pyopenwisp_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
Truebecause.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.
| 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)() |
There was a problem hiding this comment.
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.
| 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)() |
There was a problem hiding this comment.
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.
0ac92df to
dc75288
Compare
There was a problem hiding this comment.
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:datetimeandfreeze_time.
datetime(line 1) andfreeze_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 BaseTestCaseopenwisp_radius/tests/test_commands.py (1)
568-590: Wrapper tests are straightforward;assertIsNotNoneis redundant.The
assertIsNotNone(command)assertions on lines 572 and 583 add no value — ifCommand()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: MoveAnonymousUserimport to the top of the file.Importing
AnonymousUserinside the nestedasync 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
📒 Files selected for processing (8)
openwisp_radius/consumers.pyopenwisp_radius/tests/test_commands.pyopenwisp_radius/tests/test_consumers.pyopenwisp_radius/tests/test_counters/test_base_counter.pyopenwisp_radius/tests/test_counters/test_exceptions.pyopenwisp_radius/tests/test_counters/test_resets.pyopenwisp_radius/tests/test_counters/test_sqlite_counters.pyopenwisp_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
SkipCheckandMaxQuotaReachedare all correct and aligned with the source inopenwisp_radius/counters/exceptions.py.openwisp_radius/consumers.py (1)
14-16: Good fix — theexists()result is now properly returned.The previous implementation discarded the
.exists()boolean and always returnedTruefor 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_batchtests 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 theexists()-based fix inconsumers.py.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| 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") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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: PreferassertGreaterEqual/assertInfor better failure messages.
assertTrue(len(dicts) >= 1)andassertTrue("dictionary" in dicts[0])produce unhelpfulAssertionError: False is not trueon 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 ofDisconnectNAK.
DisconnectNAKis 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_initis essentially a duplicate oftest_sqlite_organization_id_format.Both tests create a
DailyCounterwith"Max-Daily-Session"kwargs, assert thatcounter.organization_idcontains no hyphens, and assert it equalsstr(group.organization_id).replace("-", ""). The only difference is thattest_sqlite_counter_mixin_initadditionally asserts the original UUID contains hyphens, which is a tautological check on Python'sstr(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_propertyonly 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:TestOrganizationMixinis redundant in the base class list.
CreateRadiusObjectsMixinalready inherits fromTestOrganizationMixin(seeopenwisp_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) andget_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 AnonymousUserThen 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
📒 Files selected for processing (6)
openwisp_radius/tests/test_consumers.pyopenwisp_radius/tests/test_counters/test_base_counter.pyopenwisp_radius/tests/test_counters/test_exceptions.pyopenwisp_radius/tests/test_counters/test_resets.pyopenwisp_radius/tests/test_counters/test_sqlite_counters.pyopenwisp_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_datasetup 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_batchsynchronously, 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) andtest_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.
| 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") |
There was a problem hiding this comment.
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.
745fb70 to
1767dcc
Compare
1767dcc to
8d88629
Compare
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
8d88629 to
57d42a6
Compare
There was a problem hiding this comment.
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
Commandand checksassertIsNotNone+assertIsInstance. SinceCommandis justclass Command(BaseConvertCalledStationIdCommand): pass, this can never fail unless the import breaks. It doesn't exercise any logic inhandle(), 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 returnsNone.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 —assertIsNotNoneis redundant and the test only checks inheritance.
Command()on line 582 will either succeed (returning a non-Noneinstance) or raise, making line 583 dead weight. TheassertIsInstancechecks 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 realprefix_add_userscommand 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.
MagicMockandBaseExceptionare imported inside the test method body. These should be at the module level for consistency and readability. Also note that importingBaseExceptionfrom the counters package shadows Python's built-inBaseException— consider using an alias.♻️ Suggested refactor
At the top of the file, add:
from unittest.mock import MagicMock from ...counters.exceptions import BaseException as CounterBaseExceptionThen 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.
DailyCounterand_acct_dataare 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_dataThen 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: MoveAnonymousUserimport 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 AnonymousUserThen remove line 89.
168-170: Moveget_channel_layerimport to the top of the file.
get_channel_layeris imported inside async test functions in bothtest_batch_status_update(line 168) andtest_disconnect_cleanup(line 200). Move it to the module level.♻️ Suggested refactor
At the top of the file:
+from channels.layers import get_channel_layerThen remove lines 168 and 200.
Also applies to: 200-202
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
openwisp_radius/consumers.pyopenwisp_radius/tests/test_commands.pyopenwisp_radius/tests/test_consumers.pyopenwisp_radius/tests/test_counters/test_base_counter.pyopenwisp_radius/tests/test_counters/test_exceptions.pyopenwisp_radius/tests/test_counters/test_resets.pyopenwisp_radius/tests/test_counters/test_sqlite_counters.pyopenwisp_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_subscriptionbranch whereday_joined=4is greater than the current day (3rd), causing the start to shift to the previous month. Expected values align with the logic inresets.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")andday_joined=15, the start is Nov 15 (not in the future relative to Nov 30), yieldingstart="2021-11-15"andend="2021-12-15".openwisp_radius/tests/test_consumers.py (2)
211-239: Good coverage of_user_can_access_batchacross 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.
| 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) |
There was a problem hiding this comment.
Duplicate assertion and deprecated API.
- Line 117 (
self.assertEqual(start, 0)) is an exact duplicate of line 115. - Line 118 uses
datetime.utcfromtimestamp, which is deprecated since Python 3.12. Usedatetime.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.
Implement tests for RadiusBatchConsumer to verify authentication, authorization, and group messaging logic.
Increases coverage for consumers.py to 84%.
Checklist
Reference to Existing Issue
Closes #677