diff --git a/CHANGELOG.md b/CHANGELOG.md index 6afab76..3a53b69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## v0.2.2 (2026-03-28) +### Bug Fixes + +- **Test Connection always showed "success"**: frontend never checked the `success` field + returned by the backend — any HTTP 200 response (including `{success: false}`) was + displayed as "Connection successful!". Now the actual `success` value is checked and + authentication failures are shown as errors with the server's message. +- **Test Connection in edit mode required password re-entry**: added backend endpoint + `POST /mail-accounts/{account_id}/test` that decrypts and uses the stored credentials + so existing accounts can be tested without re-typing the password. + + + ### Bug Fixes - Resolve semantic-release CHANGELOG.md not updating properly diff --git a/backend/app/api/v1/endpoints/mail_accounts.py b/backend/app/api/v1/endpoints/mail_accounts.py index a846041..7c9c9bf 100644 --- a/backend/app/api/v1/endpoints/mail_accounts.py +++ b/backend/app/api/v1/endpoints/mail_accounts.py @@ -8,7 +8,7 @@ from sqlalchemy import select, desc, func from app.core.database import get_db from app.core.deps import get_current_active_user -from app.core.security import encrypt_credential +from app.core.security import encrypt_credential, decrypt_credential from app.models.database_models import ( User, MailAccount, @@ -269,6 +269,39 @@ async def test_mail_connection( return MailAccountTestResponse(success=success, message=message) +@router.post("/{account_id}/test", response_model=MailAccountTestResponse) +async def test_existing_mail_connection( + account_id: int, + current_user: User = Depends(get_current_active_user), + db: AsyncSession = Depends(get_db), +): + """Test connection for an existing mail account using its stored credentials""" + result = await db.execute( + select(MailAccount).where( + MailAccount.id == account_id, MailAccount.user_id == current_user.id + ) + ) + account = result.scalar_one_or_none() + + if not account: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Mail account not found" + ) + + try: + password = decrypt_credential(account.encrypted_password) + except Exception: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to decrypt stored credentials", + ) + + processor = MailProcessor(account, password) + success, message = await processor.test_connection() + + return MailAccountTestResponse(success=success, message=message) + + @router.post("/auto-detect", response_model=MailAccountAutoDetectResponse) async def auto_detect_mail_settings( detect_request: MailAccountAutoDetectRequest, diff --git a/docs/TODO.md b/docs/TODO.md index cebd7e5..a563c72 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -17,7 +17,8 @@ Comprehensive task breakdown for repository improvements and production readines - [x] Added GitOps auto-deployment step in `ci.yml` to update preprod k8s manifest in `k8s-cluster-state` repo. - [x] Fixed GitOps `update-k8s-manifest` job: added PAT availability check to skip gracefully when `GH_PAT` secret is not configured, fixing 403 "Write access to repository not granted" pipeline failure. - [x] Fixed Celery `TypeError: can't subtract offset-naive and offset-aware datetimes` in `process_all_enabled_accounts` — all mail accounts were silently skipped on every scheduled run. -- [x] Changed Celery beat schedule to run `process_all_enabled_accounts` every minute so accounts configured with `check_interval_minutes = 1` are polled as expected. +- [x] Fixed **Test Connection** always reporting success regardless of authentication outcome. +- [x] Added `POST /mail-accounts/{account_id}/test` endpoint to test existing accounts with stored credentials. ## 🔴 Critical - Security (In Progress) diff --git a/frontend/src/components/AddMailAccountModal.tsx b/frontend/src/components/AddMailAccountModal.tsx index 30059ef..16f12de 100644 --- a/frontend/src/components/AddMailAccountModal.tsx +++ b/frontend/src/components/AddMailAccountModal.tsx @@ -115,24 +115,37 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro }; const handleTestConnection = async () => { - if (!formData.username || !formData.password || !formData.host) { - alert('Please fill in username, password, and host'); - return; - } - setTestStatus('testing'); setTestMessage(''); try { - await mailAccountsApi.test({ - protocol: formData.protocol, - host: formData.host, - port: formData.port, - username: formData.username, - password: formData.password, - use_ssl: formData.use_ssl, - }); - setTestStatus('success'); - setTestMessage('Connection successful!'); + let result: { success: boolean; message: string }; + + if (isEditMode && !formData.password && account?.id) { + // Edit mode with no new password entered — test using stored credentials + result = await mailAccountsApi.testExisting(account.id); + } else { + if (!formData.username || !formData.password || !formData.host) { + setTestStatus('error'); + setTestMessage('Please fill in username, password, and host'); + return; + } + result = await mailAccountsApi.test({ + protocol: formData.protocol, + host: formData.host, + port: formData.port, + username: formData.username, + password: formData.password, + use_ssl: formData.use_ssl, + }); + } + + if (result.success) { + setTestStatus('success'); + setTestMessage(result.message); + } else { + setTestStatus('error'); + setTestMessage(result.message || 'Connection failed'); + } } catch (error) { setTestStatus('error'); const errorMessage = error instanceof Error && 'response' in error diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 8dfc15a..1c61637 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -338,6 +338,13 @@ export const mailAccountsApi = { return response.data; }, + async testExisting(accountId: number): Promise<{ success: boolean; message: string }> { + const response = await api.post<{ success: boolean; message: string }>( + `/mail-accounts/${accountId}/test` + ); + return response.data; + }, + async autoDetect( emailAddress: string ): Promise<{ success: boolean; suggestions: AutoDetectSuggestion[] }> {