Skip to content

Upgrade/phase-1-python-312#101

Merged
DaTiC0 merged 14 commits into
mainfrom
upgrade/phase-1-python-312
Mar 11, 2026
Merged

Upgrade/phase-1-python-312#101
DaTiC0 merged 14 commits into
mainfrom
upgrade/phase-1-python-312

Conversation

@DaTiC0

@DaTiC0 DaTiC0 commented Mar 9, 2026

Copy link
Copy Markdown
Owner

Replace all 11 print() statements with structured logger.debug() calls:

  • get_current_user(): log user lookup results
  • load_client(): log client ID and lookup results
  • load_grant(): log grant request
  • save_grant(): log grant creation
  • load_token(): log token request
  • save_token(): log token creation and existing tokens

Maintains consistency with logging infrastructure added in earlier Phase 1 commits. All tests passing (10/10).

Summary

  • What changed?
  • Why was it needed?

Validation

  • Local checks passed (make test)
  • Runtime check done where relevant (make health)
  • CI checks are green

AI-Assisted Review (if applicable)

  • I used AI assistance for parts of this change
  • I manually reviewed all AI-generated code
  • I verified no secrets/credentials were added

Risk & Rollback

  • Risk level: Low / Medium / High
  • Rollback plan (if needed):

Summary by Sourcery

Replace ad-hoc printing with structured logging, modernize OAuth and Google Home integrations, expand test coverage, and update dependencies for Python 3.12 compatibility.

Enhancements:

  • Replace print-based diagnostics with module-level structured logging across core app, OAuth, device, notification, and utility modules.
  • Adjust OAuth token/grant expiry handling to use timezone-aware datetimes compatible with newer Python versions.
  • Update Google ReportState integration to work with newer google-auth behavior and log outcomes via the standard logger.

Build:

  • Upgrade gunicorn, firebase_admin, PyMySQL, python-dotenv, and add/refresh supporting auth/network libraries (google-auth, requests, cryptography) in requirements.txt to support newer Python runtimes.

Tests:

  • Add integration-style tests for signup/login, device access control, and /smarthome SYNC handling.
  • Add unit-style tests covering action_devices helpers (onSync, onQuery, onExecute, request_sync) under the Flask app context.

DaTiC0 added 3 commits March 9, 2026 14:31
- Update runtime.txt to python-3.12.2
- Update non-breaking dependencies:
  - gunicorn 20.1.0 → 22.0.0 (security patches)
  - firebase-admin 3.2.1 → 6.5.0
  - pymysql 1.0.2 → 1.1.1
  - python-dotenv 0.15.0 → 1.0.0
  - Add google-auth>=2.22.0, requests>=2.31.0, cryptography>=41.0.0
- Fix Python 3.12 deprecations:
  - Replace datetime.utcnow() with datetime.now(timezone.utc) in my_oauth.py
  - Remove .decode('utf-8') from jwt.encode() in ReportState.py (google-auth 2.x returns str)
- Replace all print() statements with logging module:
  - action_devices.py: 16 print() → logger calls
  - app.py: 12 print() → logger calls
  - notifications.py: 5 print() → logger calls
  - generate_service_account_file.py: 4 print() → logger calls
  - ReportState.py: 3 print() → logger calls

Tests: All 6 tests pass ✓
Replace all 11 print() statements with structured logger.debug() calls:
- get_current_user(): log user lookup results
- load_client(): log client ID and lookup results
- load_grant(): log grant request
- save_grant(): log grant creation
- load_token(): log token request
- save_token(): log token creation and existing tokens

Maintains consistency with logging infrastructure added in earlier Phase 1 commits.
All tests passing (10/10).
@sourcery-ai

sourcery-ai Bot commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Replaces ad-hoc print-based debugging with structured logging throughout the app and OAuth code, adds runtime/tests around auth and device endpoints, modernizes JWT/report-state handling, and updates several dependencies for Python 3.12 compatibility and security.

File-Level Changes

Change Details Files
Replace print statements with structured logging across application, device actions, notifications, OAuth, and report-state modules.
  • Instantiate module-level loggers using logging.getLogger(name).
  • Convert environment/configuration, initialization, and informational prints to logger.info or logger.debug as appropriate.
  • Convert non-fatal error conditions and fallbacks to logger.warning with structured arguments instead of string interpolation.
  • Convert exception paths to logger.error, passing exception details as separate parameters for proper formatting.
  • Update MQTT and Google Home action handlers to log message flow and command handling via logger.debug/info instead of print.
action_devices.py
app.py
notifications.py
ReportState.py
generate_service_account_file.py
my_oauth.py
Adjust OAuth token/grant time handling for timezone-aware correctness and improved logging context.
  • Import timezone from datetime and use datetime.now(timezone.utc).replace(tzinfo=None) instead of datetime.utcnow for expiry calculations to avoid naive/aware mismatches.
  • Log created Grant and Token objects as well as existing tokens when saving, to aid debugging of auth flows.
  • Log client and user resolution during OAuth client/token lookup instead of printing raw objects.
my_oauth.py
Modernize ReportState JWT usage in line with updated google-auth behavior and structured logging.
  • Initialize a logger for the ReportState module and log high-level state reporting steps and success/failure outcomes.
  • Treat generate_jwt return value as a string (no .decode) to match newer google-auth versions.
  • Log the Google Home report_state HTTP response body at info level and failures at error level.
ReportState.py
Add integration-style tests for auth, device endpoints, and action_devices helpers, plus test-time DB cleanup.
  • Introduce AuthTests to exercise signup and login flows, including redirect handling and profile rendering.
  • Introduce DeviceEndpointTests to assert login requirement for /devices and basic /smarthome SYNC behavior.
  • Introduce ActionDevicesUnitTests that call onSync, onQuery, onExecute, and request_sync under an app context and validate basic contract/return shapes.
  • Add SQLAlchemy select import and teardown logic to delete the test user created during auth tests to keep the test DB clean.
tests/test_app.py
Update dependencies to versions compatible with Python 3.12 and newer google-auth/firebase stacks.
  • Bump gunicorn from 20.1.0 to 22.0.0.
  • Update firebase_admin from 3.2.1 to 6.5.0 and add explicit google-auth, requests, and cryptography minimum versions required by the newer stack.
  • Bump PyMySQL to 1.1.1 and python-dotenv to 1.0.0.
  • Retain existing Flask and related constraints while extending requirements for better security and compatibility.
requirements.txt
runtime.txt

Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request primarily focuses on upgrading the project's Python environment to version 3.12.2 and modernizing its logging practices. It systematically replaces all direct print() statements with structured logging calls, ensuring better log management and debugging capabilities. Concurrently, it updates various core dependencies to their latest compatible versions and introduces new ones, addressing compatibility requirements for Python 3.12. The changes also include adjustments to datetime handling for timezone awareness and the addition of new, comprehensive unit tests for critical application components.

Highlights

  • Logging Modernization: Replaced all print() statements across the codebase with structured logger.debug(), logger.info(), and logger.warning() calls to improve observability and maintain consistency with existing logging infrastructure.
  • Python 3.12 Compatibility: Updated the project's runtime to Python 3.12.2 and adjusted datetime.utcnow() calls to datetime.now(timezone.utc).replace(tzinfo=None) for timezone-aware datetime handling, aligning with Python 3.12 best practices.
  • Dependency Updates: Upgraded several key dependencies including gunicorn, firebase_admin, PyMySQL, and python-dotenv, and introduced new dependencies like google-auth, requests, and cryptography to support the Python 3.12 environment and modern library versions.
  • Enhanced Test Coverage: Introduced new unit test classes (AuthTests, DeviceEndpointTests, ActionDevicesUnitTests) in tests/test_app.py to improve the reliability and maintainability of authentication and device interaction functionalities.
Changelog
  • ReportState.py
    • Replaced print statements with logger.info and logger.error for structured logging.
    • Modified generate_jwt call to remove .decode("utf-8") as google-auth >= 2.x returns a string.
  • action_devices.py
    • Integrated logging module and replaced print statements with logger.warning, logger.debug, and logger.error.
  • app.py
    • Configured basic logging and replaced print statements with logger.info and logger.warning for application startup and configuration messages.
  • generate_service_account_file.py
    • Incorporated logging and replaced print statements with logger.debug and logger.warning for output related to service account file generation.
  • my_oauth.py
    • Introduced logging and timezone from datetime.
    • Replaced print statements with logger.debug for OAuth-related operations.
    • Updated datetime.utcnow() to datetime.now(timezone.utc).replace(tzinfo=None) for Python 3.12 compatibility.
  • notifications.py
    • Added logging module and replaced print statements with logger.debug for MQTT message handling.
  • requirements.txt
    • Updated gunicorn from 20.1.0 to 22.0.0.
    • Updated firebase_admin from 3.2.1 to 6.5.0.
    • Updated PyMySQL from 1.0.2 to 1.1.1.
    • Updated python-dotenv from 0.15.0 to 1.0.0.
    • Added google-auth>=2.22.0, requests>=2.31.0, and cryptography>=41.0.0.
  • runtime.txt
    • Updated Python version from 3.8.9 to 3.12.2.
  • tests/test_app.py
    • Imported sqlalchemy.select for database cleanup in tests.
    • Added AuthTests class for signup and login functionality.
    • Added DeviceEndpointTests class for device endpoint access.
    • Added ActionDevicesUnitTests class for action_devices module functions.
Activity
  • All existing tests are passing (10/10), indicating no regressions from the changes.
  • The author explicitly mentioned validation steps in the PR description, including local checks (make test), runtime checks (make health), and CI checks.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Comment thread action_devices.py Fixed

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 4 issues, and left some high level feedback:

  • In my_oauth.py, prefer logger format arguments (e.g., logger.debug('Current user: %s', user)) instead of f-strings to avoid doing string interpolation when the log level is disabled and to keep logging style consistent with the rest of the changes.
  • The new use of datetime.now(timezone.utc).replace(tzinfo=None) in save_grant and save_token is a bit convoluted; if you want a naive UTC timestamp similar to the previous behavior, consider keeping datetime.utcnow() for clarity or making the timestamps fully timezone-aware and updating the DB model accordingly.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `my_oauth.py`, prefer logger format arguments (e.g., `logger.debug('Current user: %s', user)`) instead of f-strings to avoid doing string interpolation when the log level is disabled and to keep logging style consistent with the rest of the changes.
- The new use of `datetime.now(timezone.utc).replace(tzinfo=None)` in `save_grant` and `save_token` is a bit convoluted; if you want a naive UTC timestamp similar to the previous behavior, consider keeping `datetime.utcnow()` for clarity or making the timestamps fully timezone-aware and updating the DB model accordingly.

## Individual Comments

### Comment 1
<location path="ReportState.py" line_range="55-56" />
<code_context>
-    print('By ReportState')
-    signed_jwt = generate_jwt(service_account).decode("utf-8")  # Decode
+    logger.info('By ReportState')
+    # google-auth >= 2.x returns a str; no .decode() needed
+    signed_jwt = generate_jwt(service_account)
     access_token = get_access_token(signed_jwt)
     success = report_state(access_token, report_state_file)
</code_context>
<issue_to_address>
**issue (bug_risk):** The removal of `.decode("utf-8")` may break compatibility if `jwt.encode` still returns bytes.

`google.auth.jwt.encode` has historically returned `bytes`, and some environments may still see that behavior. If `generate_jwt` returns `bytes`, dropping `.decode("utf-8")` will make `signed_jwt` a `bytes` value and can break `get_access_token` or any consumers that expect a string (e.g. headers/JSON). Please either keep the explicit `.decode("utf-8")` or guarantee (and document) that `generate_jwt` always returns a `str`, so behavior doesn’t silently depend on the installed `google-auth` version.
</issue_to_address>

### Comment 2
<location path="tests/test_app.py" line_range="110-115" />
<code_context>
+                db.session.delete(result)
+                db.session.commit()
+
+    def test_signup_and_login(self):
+        resp = self.client.post('/signup', data={'email': 'a@b.com', 'name': 'A', 'password': 'pass'})
+        self.assertIn(resp.status_code, (302, 200))
+        resp2 = self.client.post('/login', data={'email': 'a@b.com', 'password': 'pass'}, follow_redirects=True)
+        self.assertEqual(resp2.status_code, 200)
+        self.assertIn('Profile', resp2.get_data(as_text=True))
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add negative-path auth tests (e.g., bad password, duplicate email, missing fields) to better cover the signup/login flows.

Currently we only cover the happy path for signup and login. Please add tests for at least:
- Signup with an email that already exists (should fail with a clear error).
- Login with an incorrect password (should not authenticate and should return the expected status/flash).
Optionally, also cover signup with missing fields to assert validation behavior and error responses.

Suggested implementation:

```python
    def test_signup_and_login(self):
        resp = self.client.post(
            '/signup',
            data={'email': 'a@b.com', 'name': 'A', 'password': 'pass'},
        )
        self.assertIn(resp.status_code, (302, 200))
        resp2 = self.client.post(
            '/login',
            data={'email': 'a@b.com', 'password': 'pass'},
            follow_redirects=True,
        )
        self.assertEqual(resp2.status_code, 200)
        self.assertIn('Profile', resp2.get_data(as_text=True))

    def test_signup_with_duplicate_email_fails(self):
        # First signup should succeed
        first_resp = self.client.post(
            '/signup',
            data={'email': 'a@b.com', 'name': 'A', 'password': 'pass'},
            follow_redirects=True,
        )
        self.assertIn(first_resp.status_code, (200, 302))

        # Second signup with the same email should fail with a clear error
        second_resp = self.client.post(
            '/signup',
            data={'email': 'a@b.com', 'name': 'A2', 'password': 'pass2'},
            follow_redirects=True,
        )

        # Request should be handled but not treated as a successful signup
        self.assertEqual(second_resp.status_code, 200)
        body = second_resp.get_data(as_text=True)
        self.assertIn('Email already', body)

    def test_login_with_incorrect_password_fails(self):
        # Create a valid user
        signup_resp = self.client.post(
            '/signup',
            data={'email': 'a@b.com', 'name': 'A', 'password': 'correct-pass'},
            follow_redirects=True,
        )
        self.assertIn(signup_resp.status_code, (200, 302))

        # Attempt to login with an incorrect password
        login_resp = self.client.post(
            '/login',
            data={'email': 'a@b.com', 'password': 'wrong-pass'},
            follow_redirects=True,
        )

        # User should not be authenticated; login page should be re-rendered with an error
        self.assertEqual(login_resp.status_code, 200)
        body = login_resp.get_data(as_text=True)
        self.assertIn('Invalid email or password', body)

    def test_signup_missing_required_fields_shows_errors(self):
        # Missing email
        missing_email_resp = self.client.post(
            '/signup',
            data={'email': '', 'name': 'A', 'password': 'pass'},
            follow_redirects=True,
        )
        self.assertEqual(missing_email_resp.status_code, 200)
        body = missing_email_resp.get_data(as_text=True)
        self.assertIn('Email', body)

        # Missing password
        missing_password_resp = self.client.post(
            '/signup',
            data={'email': 'a@b.com', 'name': 'A', 'password': ''},
            follow_redirects=True,
        )
        self.assertEqual(missing_password_resp.status_code, 200)
        body = missing_password_resp.get_data(as_text=True)
        self.assertIn('Password', body)


```

You may need to adjust:
1. The exact error message substrings (`'Email already'`, `'Invalid email or password'`, `'Email'`, `'Password'`) to match the actual flash/validation messages your views render.
2. The expected status codes if your signup/login endpoints return something other than 200 on validation failure (e.g., 400).
3. If your teardown only deletes a single email and you introduce other test emails elsewhere, extend the teardown to clean up any additional test users you create.
</issue_to_address>

### Comment 3
<location path="tests/test_app.py" line_range="123-125" />
<code_context>
+        flask_app.config.update(TESTING=True)
+        self.client = flask_app.test_client()
+
+    def test_devices_requires_login(self):
+        resp = self.client.get('/devices')
+        self.assertEqual(resp.status_code, 302)
+
+    def test_smarthome_sync(self):
</code_context>
<issue_to_address>
**suggestion (testing):** Consider asserting on the redirect target and guarding this test with FULL_FEATURES so it behaves predictably across environments.

Right now this only checks for a 302; please also assert the redirect `Location` (e.g., login page) to directly confirm the auth requirement for `/devices`. In addition, consider `@unittest.skipUnless(FULL_FEATURES, "Devices route requires full feature set")` so this test doesn’t fail in minimal configurations where `/devices` behaves differently.

Suggested implementation:

```python
    @unittest.skipUnless(FULL_FEATURES, "Devices route requires full feature set")
    def test_devices_requires_login(self):
        resp = self.client.get('/devices')
        self.assertEqual(resp.status_code, 302)
        location = resp.headers.get('Location', '')
        self.assertIn('/login', location)

```

If `FULL_FEATURES` is not already imported or defined in `tests/test_app.py`, you will also need to:
1. Import or reference it consistently with the rest of the test suite, for example:
   - `from app import FULL_FEATURES` or
   - `from your_module import FULL_FEATURES`
2. Ensure `unittest` is imported at the top of the file (it likely already is since `unittest.TestCase` is used).
</issue_to_address>

### Comment 4
<location path="tests/test_app.py" line_range="139-148" />
<code_context>
+    def setUp(self):
+        flask_app.config.update(TESTING=True)
+
+    def test_onSync_and_helpers(self):
+        with flask_app.app_context():
+            from action_devices import onSync, onQuery, onExecute, request_sync
+            sync = onSync()
+            self.assertIn('agentUserId', sync)
+            q = onQuery({'inputs': [{'payload': {'devices': [{'id': 'nonexistent'}]}}]})
+            self.assertIn('devices', q)
+            exec_resp = onExecute({'inputs': [{'payload': {'devices': [{'id': 'nonexistent'}]},
+                                               'commands': [{'execution': [{'command': 'action.devices.commands.OnOff',
+                                                                            'params': {'on': True}}]}]}]})
+            self.assertIn('commands', exec_resp)
+            self.assertEqual(exec_resp['commands'][0]['status'], 'ERROR')
+            self.assertFalse(request_sync('', ''))
</code_context>
<issue_to_address>
**suggestion (testing):** Avoid real external calls in `request_sync` and add more granular assertions for the action_devices helpers.

The test currently risks making a real HTTP call via `request_sync('', '')`. Please mock `requests.post` (e.g., with `unittest.mock.patch`) so it stays a true unit test without external dependencies.

Also consider splitting this into smaller tests per helper (`onSync`, `onQuery`, `onExecute`, `request_sync`) and add targeted assertions, such as checking specific error statuses/errorCodes for malformed `onExecute` commands and covering both existing and non-existing device IDs in `onQuery`. This will give more precise coverage of error handling and helper behavior.

Suggested implementation:

```python
class ActionDevicesUnitTests(unittest.TestCase):
    def setUp(self):
        flask_app.config.update(TESTING=True)
        # Use a real app context so action_devices helpers can access Flask config/state.
        self.app_context = flask_app.app_context()
        self.app_context.push()

    def tearDown(self):
        # Ensure app context is cleaned up after each test.
        self.app_context.pop()

    def test_onSync_returns_agent_user_id(self):
        from action_devices import onSync

        sync = onSync()

        self.assertIsInstance(sync, dict)
        self.assertIn('agentUserId', sync)

    def test_onQuery_handles_existing_and_nonexistent_devices(self):
        from action_devices import onQuery

        # Use a mix of existing and non-existing IDs to exercise error handling.
        # NOTE: adjust `existing_device_id` to match a real device ID from action_devices.
        existing_device_id = 'existing-device-id'
        nonexistent_device_id = 'nonexistent-device-id'

        query_payload = {
            'inputs': [
                {
                    'payload': {
                        'devices': [
                            {'id': existing_device_id},
                            {'id': nonexistent_device_id},
                        ]
                    }
                }
            ]
        }

        q = onQuery(query_payload)

        self.assertIn('devices', q)
        devices = q['devices']
        self.assertIsInstance(devices, dict)

        # Existing device should be present and not in an error state (details depend on implementation).
        self.assertIn(existing_device_id, devices)

        # Non-existent device should be present with an error status / errorCode if supported.
        self.assertIn(nonexistent_device_id, devices)
        nonexistent_info = devices[nonexistent_device_id]
        self.assertIsInstance(nonexistent_info, dict)
        if 'status' in nonexistent_info:
            self.assertEqual(nonexistent_info['status'], 'ERROR')
        if 'errorCode' in nonexistent_info:
            # Typical GSH error for missing device; adjust if implementation differs.
            self.assertIn(nonexistent_info['errorCode'], ('deviceNotFound', 'notFound', 'unknownError'))

    def test_onExecute_returns_error_for_invalid_device_or_command(self):
        from action_devices import onExecute

        execute_payload = {
            'inputs': [
                {
                    'payload': {
                        'devices': [{'id': 'nonexistent-device-id'}],
                    },
                    'commands': [
                        {
                            'execution': [
                                {
                                    'command': 'action.devices.commands.OnOff',
                                    'params': {'on': True},
                                }
                            ]
                        }
                    ],
                }
            ]
        }

        exec_resp = onExecute(execute_payload)

        self.assertIn('commands', exec_resp)
        commands = exec_resp['commands']
        self.assertIsInstance(commands, list)
        self.assertGreaterEqual(len(commands), 1)

        cmd_result = commands[0]
        self.assertIsInstance(cmd_result, dict)
        self.assertEqual(cmd_result.get('status'), 'ERROR')
        # Ensure errorCode is surfaced when available.
        if 'errorCode' in cmd_result:
            self.assertIsInstance(cmd_result['errorCode'], str)
            self.assertTrue(cmd_result['errorCode'])

    def test_request_sync_handles_successful_response(self):
        from unittest.mock import patch, MagicMock
        from action_devices import request_sync

        with patch('action_devices.requests.post') as mock_post:
            mock_response = MagicMock()
            mock_response.status_code = 200
            mock_post.return_value = mock_response

            result = request_sync('test-agent-user', 'test-api-key')

            # Should treat HTTP 200 as success.
            self.assertTrue(result)
            mock_post.assert_called_once()
            args, kwargs = mock_post.call_args
            self.assertIsInstance(args[0], str)  # URL
            self.assertIn('headers', kwargs)
            self.assertIn('json', kwargs)

    def test_request_sync_handles_error_response(self):
        from unittest.mock import patch, MagicMock
        from action_devices import request_sync

        with patch('action_devices.requests.post') as mock_post:
            mock_response = MagicMock()
            mock_response.status_code = 500
            mock_post.return_value = mock_response

            result = request_sync('test-agent-user', 'test-api-key')

            # Should not raise and should return False on non-200 responses.
            self.assertFalse(result)
            mock_post.assert_called_once()

```

1. The `existing_device_id` value in `test_onQuery_handles_existing_and_nonexistent_devices` must be updated to match a real device ID defined/known in `action_devices` (e.g. from a devices registry or fixture). The test currently assumes such an ID exists.
2. The structure of the `onQuery` and `onExecute` responses is inferred. If the actual implementation nests results under a `payload` key or uses a different shape, adjust the assertions accordingly (e.g., `q['payload']['devices']` instead of `q['devices']`).
3. The `request_sync` tests assume `request_sync` returns `True` on HTTP 200 and `False` on non-200 responses, and that `action_devices` imports `requests` as `import requests`. If `request_sync` uses a different success criterion or a different import path, update the patch target (`'action_devices.requests.post'`) and expected return values.
</issue_to_address>

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

Comment thread ReportState.py Outdated
Comment thread tests/test_app.py
Comment on lines +110 to +115
def test_signup_and_login(self):
resp = self.client.post('/signup', data={'email': 'a@b.com', 'name': 'A', 'password': 'pass'})
self.assertIn(resp.status_code, (302, 200))
resp2 = self.client.post('/login', data={'email': 'a@b.com', 'password': 'pass'}, follow_redirects=True)
self.assertEqual(resp2.status_code, 200)
self.assertIn('Profile', resp2.get_data(as_text=True))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Add negative-path auth tests (e.g., bad password, duplicate email, missing fields) to better cover the signup/login flows.

Currently we only cover the happy path for signup and login. Please add tests for at least:

  • Signup with an email that already exists (should fail with a clear error).
  • Login with an incorrect password (should not authenticate and should return the expected status/flash).
    Optionally, also cover signup with missing fields to assert validation behavior and error responses.

Suggested implementation:

    def test_signup_and_login(self):
        resp = self.client.post(
            '/signup',
            data={'email': 'a@b.com', 'name': 'A', 'password': 'pass'},
        )
        self.assertIn(resp.status_code, (302, 200))
        resp2 = self.client.post(
            '/login',
            data={'email': 'a@b.com', 'password': 'pass'},
            follow_redirects=True,
        )
        self.assertEqual(resp2.status_code, 200)
        self.assertIn('Profile', resp2.get_data(as_text=True))

    def test_signup_with_duplicate_email_fails(self):
        # First signup should succeed
        first_resp = self.client.post(
            '/signup',
            data={'email': 'a@b.com', 'name': 'A', 'password': 'pass'},
            follow_redirects=True,
        )
        self.assertIn(first_resp.status_code, (200, 302))

        # Second signup with the same email should fail with a clear error
        second_resp = self.client.post(
            '/signup',
            data={'email': 'a@b.com', 'name': 'A2', 'password': 'pass2'},
            follow_redirects=True,
        )

        # Request should be handled but not treated as a successful signup
        self.assertEqual(second_resp.status_code, 200)
        body = second_resp.get_data(as_text=True)
        self.assertIn('Email already', body)

    def test_login_with_incorrect_password_fails(self):
        # Create a valid user
        signup_resp = self.client.post(
            '/signup',
            data={'email': 'a@b.com', 'name': 'A', 'password': 'correct-pass'},
            follow_redirects=True,
        )
        self.assertIn(signup_resp.status_code, (200, 302))

        # Attempt to login with an incorrect password
        login_resp = self.client.post(
            '/login',
            data={'email': 'a@b.com', 'password': 'wrong-pass'},
            follow_redirects=True,
        )

        # User should not be authenticated; login page should be re-rendered with an error
        self.assertEqual(login_resp.status_code, 200)
        body = login_resp.get_data(as_text=True)
        self.assertIn('Invalid email or password', body)

    def test_signup_missing_required_fields_shows_errors(self):
        # Missing email
        missing_email_resp = self.client.post(
            '/signup',
            data={'email': '', 'name': 'A', 'password': 'pass'},
            follow_redirects=True,
        )
        self.assertEqual(missing_email_resp.status_code, 200)
        body = missing_email_resp.get_data(as_text=True)
        self.assertIn('Email', body)

        # Missing password
        missing_password_resp = self.client.post(
            '/signup',
            data={'email': 'a@b.com', 'name': 'A', 'password': ''},
            follow_redirects=True,
        )
        self.assertEqual(missing_password_resp.status_code, 200)
        body = missing_password_resp.get_data(as_text=True)
        self.assertIn('Password', body)

You may need to adjust:

  1. The exact error message substrings ('Email already', 'Invalid email or password', 'Email', 'Password') to match the actual flash/validation messages your views render.
  2. The expected status codes if your signup/login endpoints return something other than 200 on validation failure (e.g., 400).
  3. If your teardown only deletes a single email and you introduce other test emails elsewhere, extend the teardown to clean up any additional test users you create.

Comment thread tests/test_app.py
Comment on lines +123 to +125
def test_devices_requires_login(self):
resp = self.client.get('/devices')
self.assertEqual(resp.status_code, 302)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Consider asserting on the redirect target and guarding this test with FULL_FEATURES so it behaves predictably across environments.

Right now this only checks for a 302; please also assert the redirect Location (e.g., login page) to directly confirm the auth requirement for /devices. In addition, consider @unittest.skipUnless(FULL_FEATURES, "Devices route requires full feature set") so this test doesn’t fail in minimal configurations where /devices behaves differently.

Suggested implementation:

    @unittest.skipUnless(FULL_FEATURES, "Devices route requires full feature set")
    def test_devices_requires_login(self):
        resp = self.client.get('/devices')
        self.assertEqual(resp.status_code, 302)
        location = resp.headers.get('Location', '')
        self.assertIn('/login', location)

If FULL_FEATURES is not already imported or defined in tests/test_app.py, you will also need to:

  1. Import or reference it consistently with the rest of the test suite, for example:
    • from app import FULL_FEATURES or
    • from your_module import FULL_FEATURES
  2. Ensure unittest is imported at the top of the file (it likely already is since unittest.TestCase is used).

Comment thread tests/test_app.py
Comment on lines +139 to +148
def test_onSync_and_helpers(self):
with flask_app.app_context():
from action_devices import onSync, onQuery, onExecute, request_sync
sync = onSync()
self.assertIn('agentUserId', sync)
q = onQuery({'inputs': [{'payload': {'devices': [{'id': 'nonexistent'}]}}]})
self.assertIn('devices', q)
exec_resp = onExecute({'inputs': [{'payload': {'devices': [{'id': 'nonexistent'}]},
'commands': [{'execution': [{'command': 'action.devices.commands.OnOff',
'params': {'on': True}}]}]}]})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Avoid real external calls in request_sync and add more granular assertions for the action_devices helpers.

The test currently risks making a real HTTP call via request_sync('', ''). Please mock requests.post (e.g., with unittest.mock.patch) so it stays a true unit test without external dependencies.

Also consider splitting this into smaller tests per helper (onSync, onQuery, onExecute, request_sync) and add targeted assertions, such as checking specific error statuses/errorCodes for malformed onExecute commands and covering both existing and non-existing device IDs in onQuery. This will give more precise coverage of error handling and helper behavior.

Suggested implementation:

class ActionDevicesUnitTests(unittest.TestCase):
    def setUp(self):
        flask_app.config.update(TESTING=True)
        # Use a real app context so action_devices helpers can access Flask config/state.
        self.app_context = flask_app.app_context()
        self.app_context.push()

    def tearDown(self):
        # Ensure app context is cleaned up after each test.
        self.app_context.pop()

    def test_onSync_returns_agent_user_id(self):
        from action_devices import onSync

        sync = onSync()

        self.assertIsInstance(sync, dict)
        self.assertIn('agentUserId', sync)

    def test_onQuery_handles_existing_and_nonexistent_devices(self):
        from action_devices import onQuery

        # Use a mix of existing and non-existing IDs to exercise error handling.
        # NOTE: adjust `existing_device_id` to match a real device ID from action_devices.
        existing_device_id = 'existing-device-id'
        nonexistent_device_id = 'nonexistent-device-id'

        query_payload = {
            'inputs': [
                {
                    'payload': {
                        'devices': [
                            {'id': existing_device_id},
                            {'id': nonexistent_device_id},
                        ]
                    }
                }
            ]
        }

        q = onQuery(query_payload)

        self.assertIn('devices', q)
        devices = q['devices']
        self.assertIsInstance(devices, dict)

        # Existing device should be present and not in an error state (details depend on implementation).
        self.assertIn(existing_device_id, devices)

        # Non-existent device should be present with an error status / errorCode if supported.
        self.assertIn(nonexistent_device_id, devices)
        nonexistent_info = devices[nonexistent_device_id]
        self.assertIsInstance(nonexistent_info, dict)
        if 'status' in nonexistent_info:
            self.assertEqual(nonexistent_info['status'], 'ERROR')
        if 'errorCode' in nonexistent_info:
            # Typical GSH error for missing device; adjust if implementation differs.
            self.assertIn(nonexistent_info['errorCode'], ('deviceNotFound', 'notFound', 'unknownError'))

    def test_onExecute_returns_error_for_invalid_device_or_command(self):
        from action_devices import onExecute

        execute_payload = {
            'inputs': [
                {
                    'payload': {
                        'devices': [{'id': 'nonexistent-device-id'}],
                    },
                    'commands': [
                        {
                            'execution': [
                                {
                                    'command': 'action.devices.commands.OnOff',
                                    'params': {'on': True},
                                }
                            ]
                        }
                    ],
                }
            ]
        }

        exec_resp = onExecute(execute_payload)

        self.assertIn('commands', exec_resp)
        commands = exec_resp['commands']
        self.assertIsInstance(commands, list)
        self.assertGreaterEqual(len(commands), 1)

        cmd_result = commands[0]
        self.assertIsInstance(cmd_result, dict)
        self.assertEqual(cmd_result.get('status'), 'ERROR')
        # Ensure errorCode is surfaced when available.
        if 'errorCode' in cmd_result:
            self.assertIsInstance(cmd_result['errorCode'], str)
            self.assertTrue(cmd_result['errorCode'])

    def test_request_sync_handles_successful_response(self):
        from unittest.mock import patch, MagicMock
        from action_devices import request_sync

        with patch('action_devices.requests.post') as mock_post:
            mock_response = MagicMock()
            mock_response.status_code = 200
            mock_post.return_value = mock_response

            result = request_sync('test-agent-user', 'test-api-key')

            # Should treat HTTP 200 as success.
            self.assertTrue(result)
            mock_post.assert_called_once()
            args, kwargs = mock_post.call_args
            self.assertIsInstance(args[0], str)  # URL
            self.assertIn('headers', kwargs)
            self.assertIn('json', kwargs)

    def test_request_sync_handles_error_response(self):
        from unittest.mock import patch, MagicMock
        from action_devices import request_sync

        with patch('action_devices.requests.post') as mock_post:
            mock_response = MagicMock()
            mock_response.status_code = 500
            mock_post.return_value = mock_response

            result = request_sync('test-agent-user', 'test-api-key')

            # Should not raise and should return False on non-200 responses.
            self.assertFalse(result)
            mock_post.assert_called_once()
  1. The existing_device_id value in test_onQuery_handles_existing_and_nonexistent_devices must be updated to match a real device ID defined/known in action_devices (e.g. from a devices registry or fixture). The test currently assumes such an ID exists.
  2. The structure of the onQuery and onExecute responses is inferred. If the actual implementation nests results under a payload key or uses a different shape, adjust the assertions accordingly (e.g., q['payload']['devices'] instead of q['devices']).
  3. The request_sync tests assume request_sync returns True on HTTP 200 and False on non-200 responses, and that action_devices imports requests as import requests. If request_sync uses a different success criterion or a different import path, update the patch target ('action_devices.requests.post') and expected return values.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request successfully upgrades the application for Python 3.12 compatibility and replaces all print statements with structured logging, significantly improving maintainability and debugging. However, a critical security vulnerability has been identified: an Insecure Direct Object Reference (IDOR) / Path Traversal vulnerability in how device IDs are used to access the Firebase database, and a complete lack of authentication on the /smarthome endpoint. These issues allow unauthorized access to the entire database and control over all connected devices, requiring immediate remediation. A minor suggestion for logging best practices for performance is also noted.

Comment thread action_devices.py Outdated
Comment thread action_devices.py Outdated
Comment thread my_oauth.py Outdated
Comment thread app.py

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

old was better

…unction for schema setup and update CLI command
@DaTiC0
DaTiC0 merged commit b09d575 into main Mar 11, 2026
7 checks passed
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.

2 participants