Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace requests with NetworkClient #12096

Merged

Conversation

thesujai
Copy link
Contributor

@thesujai thesujai commented Apr 19, 2024

Summary

  • Use NetworkClient to make HTTP requests instead of requests.
  • Use the NetworkClient errors in place of regular requests exception, as the NetworkClient Errors are wrapper around them
  • Update the mocks of requests with NetworkClient in tests

References

Fixes #11018

Reviewer guidance

I did a manual test thoroughly, and mostly followed TDD.
The only place I am not sure is core.api.KolibriDataPortalViewSet as testing it required a token and no tests are written for this. Maybe we can have a issue to do this?


Testing checklist

  • Contributor has fully tested the PR manually
  • If there are any front-end changes, before/after screenshots are included
  • Critical user journeys are covered by Gherkin stories
  • Critical and brittle code paths are covered by unit tests

PR process

  • PR has the correct target branch and milestone
  • PR has 'needs review' or 'work-in-progress' label
  • If PR is ready for review, a reviewer has been added. (Don't use 'Assignees')
  • If this is an important user-facing change, PR or related issue has a 'changelog' label
  • If this includes an internal dependency change, a link to the diff is provided

Reviewer checklist

  • Automated test coverage is satisfactory
  • PR is fully functional
  • PR has been tested for accessibility regressions
  • External dependency files were updated if necessary (yarn and pip)
  • Documentation is updated
  • Contributor is in AUTHORS.md

@github-actions github-actions bot added DEV: backend Python, databases, networking, filesystem... SIZE: small labels Apr 19, 2024
@@ -30,7 +30,8 @@ def register(self, request):
facility = Facility.objects.get(id=request.data.get("facility_id"))
try:
response = registerfacility(request.data.get("token"), facility)
except requests.exceptions.RequestException as e: # bubble up any response error
# as NetworkLocationResponseFailure is a subclass of requests.exceptions.RequestException
except errors.NetworkLocationResponseFailure as e: # bubble up any response error
Copy link
Contributor Author

@thesujai thesujai Apr 19, 2024

Choose a reason for hiding this comment

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

Are we supposed to do this? Because here, requests is only used for getting the Error Class

return Response({"status": "offline"}, status=HTTP_503_SERVICE_UNAVAILABLE)
except errors.NetworkLocationResponseFailure as e:
Copy link
Contributor Author

@thesujai thesujai Apr 19, 2024

Choose a reason for hiding this comment

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

Initially i thought NetworkClient is not working properly as for this endpoint i was getting a 404, but with requests it was going smoothly for the same endpoint. Turns out there were no exception handling for 404(directly using requests).
I think endpoint should return a 400 if the token is invalid(or NONE) instead of a 404???

Copy link
Member

Choose a reason for hiding this comment

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

Looks like you could just set:
response = e.response here and let the code continue?

@thesujai
Copy link
Contributor Author

Not so straight forward as I thought:)
I replaced requests in core/content/api.py then arround 30 testcases were failing, they were KolibriStudioAPITestCase and ProxyContentMetadataTestCase.
Initially i tried directly mocking NetworkClient like below:

@mock.patch("kolibri.core.content.api.NetworkClient")
class KolibriStudioAPITestCase(APITestCase):
    @classmethod
    def setUpTestData(cls):
        DeviceSettings.objects.create(is_provisioned=True)
        cls.facility = Facility.objects.create(name="facility")
        superuser = FacilityUser.objects.create(
            username="superuser", facility=cls.facility
        )
        superuser.set_password(DUMMY_PASSWORD)
        superuser.save()
        cls.superuser = superuser
        DevicePermissions.objects.create(user=superuser, is_superuser=True)

    def setUp(self):
        self.client.login(username=self.superuser.username, password=DUMMY_PASSWORD)

    def test_channel_list(self, networkclient_mock):
        mock_response = networkclient_mock.build_for_address.return_value.get.return_value
        mock_response.json.return_value = [{"id": 1, "name": "studio"}]
        mock_response.status_code = 200

        response = networkclient_mock.build_for_address.return_value.get(
            reverse("kolibri:core:remotechannel-list"), format="json"
        )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json()[0]["id"], 1)

    def test_no_permission_non_superuser_channel_list(self, networkclient_mock):
        user = FacilityUser.objects.create(username="user", facility=self.facility)
        user.set_password(DUMMY_PASSWORD)
        user.save()
        self.client.logout()
        self.client.login(username=user.username, password=DUMMY_PASSWORD)

        mock_response = networkclient_mock.build_for_address.return_value.get.return_value
        mock_response.status_code = 403

        response = networkclient_mock.build_for_address.return_value.get(
            reverse("kolibri:core:remotechannel-list"), format="json"
        )
        self.assertEqual(response.status_code, 403)

    def test_channel_retrieve(self, networkclient_mock):
        mock_response = networkclient_mock.build_for_address.return_value.get.return_value
        mock_response.json.return_value = [{"id": 1, "name": "studio"}]
        mock_response.status_code = 200

        response = networkclient_mock.build_for_address.return_value.get(
            reverse("kolibri:core:remotechannel-list"), format="json"
        )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), [{"id": 1, "name": "studio"}])

    def test_channel_info_404(self, networkclient_mock):
        mock_response = networkclient_mock.build_for_address.return_value.get.return_value
        mock_response.status_code = 404

        response = self.client.get(
            reverse("kolibri:core:remotechannel-detail", kwargs={"pk": "abc"}),
            format="json",
        )
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

    def test_channel_info_offline(self, networkclient_mock):
        mock_response = networkclient_mock.build_for_address.return_value.get.return_value
        mock_response.status_code = 503
        mock_response.json.return_value = {"status": "offline"}

        response = networkclient_mock.build_for_address.return_value.get(
            reverse("kolibri:core:remotechannel-detail", kwargs={"pk": "abc"}),
            format="json",
        )
        self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE)
        self.assertEqual(response.json()["status"], "offline")

    def test_channel_list_offline(self, networkclient_mock):
        mock_response = networkclient_mock.build_for_address.return_value.get.return_value
        mock_response.status_code = 503
        mock_response.json.return_value = {"status": "offline"}

        response = networkclient_mock.build_for_address.return_value.get(
            reverse("kolibri:core:remotechannel-list"), format="json"
        )
        self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE)
        self.assertEqual(response.json()["status"], "offline")

    def tearDown(self):
        cache.clear()

These were passing but then what is the use of client.login etc if they are not being used, so i dropped this approach.

Are there other ways to do this?


# we cannot pass None address to get_normalized_url_variations()
# as it uses parse_address_into_components() which takes no None
if address is None:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I found some places in code using requests that the baseurl is none, so to migrate to NetworkClient without raising any error, I did this.

Copy link
Member

Choose a reason for hiding this comment

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

Are these the cases where the baseurl is the server itself? Maybe for that case we should make a build_for_localhost method or something to capture this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By server you mean https://studio.learningequality.org ?
One place i can recall where i observed the baseurl is None is this, this happens when from the browser only we try to import channel from studio.
So this behaviour(base being NONE) is already handled in get_content_server_url which our _make_channel_endpoint_request eventually calls to stack.

So for these cases instead of building network client from build_for_address which will throw an error is address is None. Can we simply create a NetworkClient obj as:

client = NetworkClient(baseurl)  # it deals with baseurl being null right?

@rtibbles
Copy link
Member

Not sure if I am completely understanding your question, but I think using a mock for the NetworkClient does seem like a good idea - the alternative would be to use something like https://requests-mock.readthedocs.io/en/latest/ - but maybe that's best used for the NetworkClient tests specifically, and then we mock the NetworkClient here.

It does seem that being able to distinguish a 404 response from other responses might be useful? As the NetworkLocationResponseFailure has the requests response attached to it as the response attribute, we can still make assertions about the status_code of that response to check for a 404.

@thesujai
Copy link
Contributor Author

thesujai commented May 1, 2024

what i actually wanted to ask is that - if you see this test case you will see in the setUp we are authenticating the client here
But in the modified testcase i provided, I am
trying to mock networkclient and do api testing, but i am not able to perform the authentication like it was doing before.

So how do i leverage the authentication(APITestCase class provided client.login()) and at the same time mock NetworkClient

@rtibbles
Copy link
Member

So how do i leverage the authentication(APITestCase class provided client.login()) and at the same time mock NetworkClient

Maybe I am missing something, but these are two separate things. The NetworkClient is only ever being used to make calls to another Kolibri, either within the course of an API request, or inside an asynchronous task. The test client used in the test cases is used to make requests to API endpoints in an efficient way (it actually kind of cheats and bypasses the http layer). So, you can still login to the test client to make requests to API endpoints, then if inside that API endpoint it is using NetworkClient to make a further request to an external Kolibri, that's where the mocking of the NetworkClient comes into play, because now instead of reaching out to the remote Kolibri (or Kolibri Studio) we just provide a mocked response from the NetworkClient.

This is good, as this is one source of flakiness for our automated tests, where if Kolibri Studio is slow to respond (either because of Studio or local network conditions), it can cause a false positive test failure.

@thesujai thesujai force-pushed the networkclient-instead-of-requests branch from 06fd5d7 to 056ea2a Compare June 15, 2024 07:13
@MisRob
Copy link
Member

MisRob commented Jul 9, 2024

Hi @thesujai, is this ready for re-review or work in progress?

@thesujai
Copy link
Contributor Author

thesujai commented Jul 9, 2024

Hello @MisRob
Can you mark this as work-in-progress? I have the changes made but it is kind of messed up so I haven't pushed it to this branch yet.

@MisRob MisRob added the work-in-progress Not ready for review label Jul 9, 2024
@MisRob
Copy link
Member

MisRob commented Jul 9, 2024

Yes, done. Thank you.

@thesujai thesujai force-pushed the networkclient-instead-of-requests branch from 056ea2a to 66cb5d0 Compare July 14, 2024 16:44
@github-actions github-actions bot added APP: User Re: User app (sign-in, sign-up, user profile, etc.) APP: Setup Wizard Re: Setup Wizard (facility import, superuser creation, settings, etc.) labels Jul 14, 2024
@thesujai thesujai changed the title Replacing requests with NetworkClient Replace requests with NetworkClient Jul 14, 2024
@thesujai
Copy link
Contributor Author

This is ready for review!

@MisRob
Copy link
Member

MisRob commented Aug 2, 2024

Thanks @thesujai

@MisRob MisRob added TODO: needs review Waiting for review and removed work-in-progress Not ready for review labels Aug 2, 2024
@rtibbles rtibbles self-assigned this Aug 6, 2024
Copy link
Member

@rtibbles rtibbles left a comment

Choose a reason for hiding this comment

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

All the code changes here make sense to me - I am not concerned about the KDP interactions, but wonder if this might mess with telemetry reporting. Will be checking locally.

@rtibbles
Copy link
Member

rtibbles commented Aug 6, 2024

Have confirmed I see no issues with telemetry pingback.

@rtibbles
Copy link
Member

rtibbles commented Aug 6, 2024

@radinamatic @pcenov to confirm no regressions here, I think the main things to regression test here are:

  • LoD resource syncing for lessons and quizzes
  • Registering and syncing with KDP
  • Importing resources from another Kolibri
  • Creating a user on a remote facility during the change facility workflow and setup wizard single learner import flows

@pcenov
Copy link
Member

pcenov commented Aug 7, 2024

Hi @thesujai @rtibbles,
After fully regression testing the above mentioned user flows I can confirm that everything seems to be functioning correctly with the exception of the change facility workflows where I am seeing 403 and 500 errors in the console at the 'Remotely integrating data' step and it takes about 3-5 minutes for the completion of the migration (initially I thought that it's not working at all). This issue is not extant in the official Kolibri 0.17 build.

Here are 3 videos (which I have shortened) with different scenarios illustrating that.

  1. Merge an existing learner:
merge.user.mp4

Logs: mergeUserLogs.zip

  1. Create a new user:
create.new.user.mp4

Logs: createNewUserLogs.zip

  1. Move account:
Move.account.mp4

Logs: moveAccountLogs.zip

@rtibbles
Copy link
Member

rtibbles commented Aug 7, 2024

Thanks @pcenov - I will double check the logs you shared and confirm whether this is caused by this PR or whether it is something that has been independently fixed, and this PR is not up to date with!

@rtibbles
Copy link
Member

rtibbles commented Aug 7, 2024

Yes - it looks like these errors and the apparently stalling migration are all things that were independently fixed in the release-v0.17.x branch that have not been merged up to here, so I think this is good to go. None of the endpoints that were modified in this PR are causing any issues!

Copy link
Member

@rtibbles rtibbles left a comment

Choose a reason for hiding this comment

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

Code review passed, manual QA has shown no regressions from these changes.

@rtibbles rtibbles merged commit 79e9e4b into learningequality:develop Aug 7, 2024
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
APP: Setup Wizard Re: Setup Wizard (facility import, superuser creation, settings, etc.) APP: User Re: User app (sign-in, sign-up, user profile, etc.) DEV: backend Python, databases, networking, filesystem... SIZE: medium SIZE: small TODO: needs review Waiting for review
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Replace existing usages of requests with NetworkClient
4 participants