Skip to content

feat: unit tests#70

Open
HarishChandran3304 wants to merge 13 commits into
version-16-hotfixfrom
feat/unit-tests
Open

feat: unit tests#70
HarishChandran3304 wants to merge 13 commits into
version-16-hotfixfrom
feat/unit-tests

Conversation

@HarishChandran3304

@HarishChandran3304 HarishChandran3304 commented Jun 10, 2026

Copy link
Copy Markdown

Description

PR to add unit tests for frappe_gmail_thread and set up the missing unit-tests CI workflow.
Also fixes one source bug found while writing tests for the Google Settings on_update doc event.
ref: App Spec

Test Cases

Gmail Account lifecycle

  • before_insert auto-fills linked_user and email_id from the session user
  • validate throws when gmail_enabled=1 but Google Settings is disabled, missing client_id, or missing client_secret
  • validate is silent when gmail_enabled=0
  • on_trash calls disable_pubsub only when enabled, has refresh_token, and topic is configured; silent otherwise
  • before_save re-validates Google Settings when gmail_enabled toggles on
  • before_save triggers sync_labels + enable_pubsub when refresh_token changes and account is enabled
  • before_save triggers disable_pubsub when refresh_token changes and account is not enabled
  • before_save resets last_historyid to 0 when labels change
  • sync_labels_api enqueues the sync job, is idempotent, and honours reset_historyid flag

Gmail Thread lifecycle

  • Status flips from Open to Linked when reference is newly set
  • Status flips from Linked back to Open when reference is cleared
  • msgprint warning fires when another thread already references the same doc
  • all_subjects is regenerated as a sorted, deduplicated, whitespace-stripped join of subject_of_first_mail and every email's subject
  • Files attached to the thread are shared via docshare with every involved user except the owner

Gmail thread sync

  • Initial sync path runs threads().list() when last_historyid=0
  • Incremental sync path runs history().list() when last_historyid is non-zero
  • Throws when gmail_enabled=0, refresh_token is empty, or no labels enabled
  • last_historyid is updated to the max history_id observed
  • notFound from the history API resets last_historyid to 0
  • Per-label exception is logged via frappe.log_error and the loop continues
  • DRAFT-labelled messages are skipped
  • AlreadyExistsError from create_new_email is swallowed and processing continues
  • New Gmail Thread is created with subject_of_first_mail, creation, and owner from the email
  • Sender, to, cc, bcc, plus the account's linked_user are aggregated into involved_users
  • publish_realtime fires on incremental sync for threads with a reference

Label sync

  • sync_labels appends new labels matched on label_id
  • DRAFT and CHAT system labels are skipped
  • should_save=True saves the account; should_save=False leaves it untouched

Permission model

  • get_permission_query_conditions returns an empty string for Administrator
  • For non-Administrator, the SQL filters Gmail Threads by Involved User membership or ownership
  • has_permission returns True for Administrator on any ptype
  • For non-Administrator, returns True for read/write/delete/create only when the user is in Involved User
  • Returns False for ptypes outside read/write/delete/create

OAuth flow

  • get_auth_url throws DoesNotExistError for a missing Gmail Account
  • get_auth_url throws PermissionError when the session user lacks write or is not the linked user
  • Returns a Google consent URL with all required OAuth parameters
  • callback throws when the session user has no Gmail Account or lacks write permission
  • authorize_access saves refresh_token + authorization_code on successful exchange
  • check_gmail_object throws when the authorized email does not match linked_user
  • check_gmail_object throws a clear authorization-expired error on invalid_grant
  • get_access_token raises ValidationError when refresh_token is missing

Realtime sync via Pub/Sub

  • enable_pubsub is a no-op when gmail_enabled=0 or realtime is off
  • enable_pubsub throws when refresh_token or topic is missing
  • enable_pubsub calls users().watch() with enabled labels + SENT (without duplicating)
  • disable_pubsub is a no-op when gmail_enabled=0 or realtime is still on
  • disable_pubsub calls users().stop() when account enabled and realtime turned off
  • pubsub.callback returns "OK" without enqueuing when Google Settings.enable=0, realtime=0, or no topic
  • pubsub.callback decodes the base64 envelope, looks up the System User, and enqueues a sync job
  • Idempotent: skips enqueue when a job is already running
  • Malformed inner JSON is logged via frappe.log_error and "OK" is still returned

Activity timeline integration

  • get_attachments_data refreshes each attachment's file_url from the File doctype
  • get_linked_gmail_threads returns one timeline entry per email for every matching thread
  • Returns empty list when no thread references the supplied doc
  • relink_gmail_thread updates reference fields and saves
  • unlink_gmail_thread clears reference fields and saves

Email parsing helpers

  • html_to_text returns BeautifulSoup-extracted text with space separator and stripped whitespace
  • pop_down_quoted_replies strips \nOn ... wrote: from text bodies
  • pop_down_quoted_replies rewraps Apple, Outlook, and Yahoo quote containers as div.gmail_quote
  • set_to_and_cc populates .to/.cc/.bcc by parsing the corresponding headers
  • find_gmail_thread matches by gmail_thread_id or walks message_ids for the parent thread
  • create_new_email raises AlreadyExistsError for a duplicate email_message_id and appends linked_user to the existing thread
  • Sets sent_or_received based on whether the sender matches a non-Website User
  • process_attachments creates private File records and renames file names >= 140 chars to <uuid>.<ext>
  • replace_inline_images rewrites cid: references to the matching File's unique_url

Scheduled tasks

  • tasks.sync.sync_emails enqueues a sync job for every enabled account with a refresh_token; idempotent
  • tasks.daily.enable_pubsub_everyday returns silently when Google Settings is disabled, realtime is off, or topic is missing
  • Calls enable_pubsub for every enabled Gmail Account; per-account failures are logged with title="PubSub Error" and the loop continues

Google Settings doc event

  • on_update calls enable_pubsub for every Gmail Account when Google Settings.custom_gmail_sync_in_realtime is truthy
  • Calls disable_pubsub when the flag is falsy
  • No-op when the flag did not change

Configuration check

  • is_gmail_configured returns {configured: False, "Gmail Account not found"} when the session user has no account
  • Throws PermissionError when the session user lacks read permission
  • Returns {configured: False, "Please configure Gmail"} when gmail_enabled=0
  • Returns {configured: True} when enabled with a refresh_token and the linked_user matches the session user

Relevant Technical Choices

  • External-API calls (Gmail discovery client, OAuth HTTP exchange, Pub/Sub push) are tested via unittest.mock patches at the consumer module's import sites; tests verify what the app passes to those layers, not the round-trip with Google.
  • The Google Settings on_update fix (gdoc to doc) lands as a separate commit on top of an intent-driven failing test, so the bug is reproducible from history.

Testing Instructions

bench --site <site> run-tests --app frappe_gmail_thread

Additional Information:

Screenshot/Screencast

Checklist

  • I have carefully reviewed the code before submitting it for review.
  • This code is adequately covered by unit tests to validate its functionality.
  • I have conducted thorough testing to ensure it functions as intended.
  • A member of the QA team has reviewed and tested this PR (To be checked by QA or code reviewer)

Fixes #

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds an integration/unit test suite for the frappe_gmail_thread app, introduces a GitHub Actions workflow to run those tests in CI, and includes a small bug fix in the Google Settings on_update doc event handler found while writing tests.

Changes:

  • Add extensive IntegrationTestCase coverage for Gmail Account, Gmail Thread sync/lifecycle, OAuth, Pub/Sub callback, activity timeline helpers, and parsing helpers.
  • Add a unit-tests.yml GitHub Actions workflow to provision bench + site and run bench run-tests --app frappe_gmail_thread.
  • Fix Google Settings doc event logic to read the realtime flag from the Google Settings doc rather than the iterated Gmail Account doc.

Reviewed changes

Copilot reviewed 14 out of 18 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
frappe_gmail_thread/tests/utils/test_helpers.py Adds tests for helper functions (email parsing, thread lookup, attachments, inline image rewriting).
frappe_gmail_thread/tests/utils/init.py Test package initializer for utils tests.
frappe_gmail_thread/tests/tasks/test_sync.py Adds tests for the scheduled sync task enqueue behavior.
frappe_gmail_thread/tests/tasks/test_daily.py Adds tests for daily Pub/Sub enabling task behavior and error logging.
frappe_gmail_thread/tests/tasks/init.py Test package initializer for tasks tests.
frappe_gmail_thread/tests/doc_events/test_google_settings.py Adds tests validating Google Settings on_update behavior (enable/disable pubsub/no-op).
frappe_gmail_thread/tests/doc_events/init.py Test package initializer for doc events tests.
frappe_gmail_thread/tests/api/test_pubsub.py Adds tests for the Pub/Sub callback endpoint behavior (gating, enqueue, malformed payload logging).
frappe_gmail_thread/tests/api/test_oauth.py Adds tests for OAuth URL generation, callback permissioning, token exchange, and pubsub enable/disable behavior.
frappe_gmail_thread/tests/api/test_gmail.py Adds tests for is_gmail_configured behavior.
frappe_gmail_thread/tests/api/test_activity.py Adds tests for timeline integration helpers (attachments refresh, link/unlink/relink).
frappe_gmail_thread/tests/api/init.py Test package initializer for API tests.
frappe_gmail_thread/tests/init.py Introduces shared test helpers (users, gmail account/thread factories, as_user).
frappe_gmail_thread/frappe_gmail_thread/doctype/gmail_thread/test_gmail_thread.py Replaces placeholder test with comprehensive Gmail Thread lifecycle/sync/permission tests.
frappe_gmail_thread/frappe_gmail_thread/doctype/gmail_account/test_gmail_account.py Replaces placeholder test with comprehensive Gmail Account lifecycle and sync enqueue tests.
frappe_gmail_thread/doc_events/google_settings.py Fixes on_update to use doc.custom_gmail_sync_in_realtime instead of reading from the iterated account doc.
.semgrepignore Minor comment spelling normalization and formatting touch-up.
.github/workflows/unit-tests.yml Adds CI workflow to set up bench/site and run the app’s test suite.

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

Comment thread frappe_gmail_thread/tests/__init__.py Outdated
Comment thread frappe_gmail_thread/tests/api/test_oauth.py Outdated
Comment thread frappe_gmail_thread/tests/tasks/test_sync.py Outdated
Comment thread frappe_gmail_thread/tests/tasks/test_sync.py Outdated
Comment thread frappe_gmail_thread/tests/tasks/test_sync.py Outdated
Comment thread frappe_gmail_thread/tests/api/test_gmail.py Outdated
Comment thread frappe_gmail_thread/tests/api/test_gmail.py Outdated
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