webvoice-mcp 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EasyTaskFlow / WebVoice
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: webvoice-mcp
3
+ Version: 0.2.0
4
+ Summary: MCP server for WebVoice — agent registration, chat, TTS, STT, translation, images
5
+ Author-email: EasyTaskFlow / WebVoice <service@easytaskflow.app>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://webvoice.easytaskflow.app
8
+ Project-URL: Documentation, https://webvoice.easytaskflow.app/api/documentation/#mcp
9
+ Project-URL: Repository, https://github.com/easytaskflow/webvoice-mcp
10
+ Project-URL: Issues, https://github.com/easytaskflow/webvoice-mcp/issues
11
+ Keywords: mcp,webvoice,tts,stt,cursor,agents,solana
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: mcp>=1.6.0
24
+ Requires-Dist: httpx>=0.27.0
25
+ Dynamic: license-file
26
+
27
+ # WebVoice MCP Server
28
+
29
+ <!-- mcp-name: io.github.easytaskflow/webvoice-mcp -->
30
+
31
+ Local [Model Context Protocol](https://modelcontextprotocol.io) server that exposes WebVoice REST API tools to **Cursor**, Claude Desktop, and other MCP clients.
32
+
33
+ **Install from PyPI:**
34
+
35
+ ```bash
36
+ pip install webvoice-mcp
37
+ ```
38
+
39
+ ### From source (development)
40
+
41
+ ```bash
42
+ pip install -r requirements-mcp.txt
43
+ # or editable install from repo root:
44
+ pip install -e .
45
+ ```
46
+
47
+ ## Configure Cursor
48
+
49
+ ### Option A — Agent registration via MCP (no browser)
50
+
51
+ 1. Add MCP **without** `WEBVOICE_API_KEY` first (or use the register tools from any client).
52
+ 2. Call **`webvoice_register_send_code`** with your email.
53
+ 3. Read the OTP from email, then **`webvoice_register_verify`** with the code.
54
+ 4. Response includes **`api_key`** (once), **`onboarding.credits`**, **`onboarding.can_use_api`**, and optional **`onboarding.solana`** (wallet + `memo_code` for USDC/SOL top-up).
55
+ 5. If `can_use_api` is true, use chat/TTS/STT immediately with welcome credits.
56
+ 6. Set `WEBVOICE_API_KEY` in MCP config and restart Cursor (optional Solana/PayPal top-up later).
57
+
58
+ REST equivalent: `POST /api/v1/auth/send-code/` → `POST /api/v1/auth/verify-code/` with `create_api_key: true`. See [API docs](https://webvoice.easytaskflow.app/api/documentation/#agent-registration).
59
+
60
+ ### Option B — Browser signup (human)
61
+
62
+ 1. **Login** — email OTP or Google (`/accounts/login/`). New users get welcome + daily free credits.
63
+ 2. **API key** — [API dashboard](https://webvoice.easytaskflow.app/api/) → Create key → copy `wv_…` (shown once).
64
+ 3. **Credits (optional)** — [Buy credits](https://webvoice.easytaskflow.app/billing/purchase/), [Premium](https://webvoice.easytaskflow.app/billing/subscriptions/) (PayPal), or **Solana** (send USDC/SOL with your personal memo from `webvoice_onboarding`).
65
+
66
+ When balance is zero, MCP calls fail with insufficient credits; you receive an email with a recharge link.
67
+
68
+ ### MCP config
69
+
70
+ Edit Cursor MCP config (`~/.cursor/mcp.json` or **Settings → MCP**):
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "webvoice": {
76
+ "command": "webvoice-mcp",
77
+ "env": {
78
+ "WEBVOICE_API_KEY": "wv_your_key_here"
79
+ }
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ If `webvoice-mcp` is not on PATH, use Python module form:
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "webvoice": {
91
+ "command": "python",
92
+ "args": ["-m", "webvoice_mcp"],
93
+ "cwd": "/path/to/webvoice",
94
+ "env": {
95
+ "WEBVOICE_API_KEY": "wv_your_key_here"
96
+ }
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ Optional: `WEBVOICE_BASE_URL` (default `https://webvoice.easytaskflow.app/api/v1`).
103
+
104
+ ## Tools
105
+
106
+ | Tool | Description |
107
+ |------|-------------|
108
+ | `webvoice_register_send_code` | Start registration — OTP to email |
109
+ | `webvoice_register_verify` | Complete registration → API key + onboarding (credits, can_use_api) |
110
+ | `webvoice_onboarding` | Credits, can_use_api, optional Solana wallet/memo, recharge URLs |
111
+ | `webvoice_status` | Credits balance |
112
+ | `webvoice_list_chat_models` | Available chat models |
113
+ | `webvoice_list_voices` | TTS voices |
114
+ | `webvoice_chat` | Chat completions (DeepSeek default) |
115
+ | `webvoice_tts` | Text-to-speech → MP3 |
116
+ | `webvoice_stt` | Transcribe local audio file |
117
+ | `webvoice_translate` | Text translation |
118
+ | `webvoice_image` | MiniMax image generation |
119
+
120
+ ## Example agent flow
121
+
122
+ **New agent (register → use → optional top-up):**
123
+
124
+ 1. `webvoice_register_send_code` → `webvoice_register_verify` → save `api_key`.
125
+ 2. If `onboarding.can_use_api`: call `webvoice_chat` / `webvoice_tts` / … immediately.
126
+ 3. Optional: `webvoice_onboarding` → Solana memo or PayPal URLs when you need more credits.
127
+
128
+ **Existing account:**
129
+
130
+ 1. Ask the model to call `webvoice_chat` with your question.
131
+ 2. Call `webvoice_tts` with `output_path` to save spoken reply.
132
+ 3. Call `webvoice_stt` with a recorded `audio_path` for voice input.
133
+
134
+ Credits are billed on your WebVoice account per API call.
@@ -0,0 +1,226 @@
1
+ # Kokoro VoiceITA - App Python per Generazione Voce Locale
2
+
3
+ App Python per generare voce usando **Kokoro TTS** completamente in locale, con supporto per la voce "Sara".
4
+
5
+ ## 🎯 Caratteristiche
6
+
7
+ - ✅ Generazione voce completamente locale (nessun invio dati a server esterni)
8
+ - ✅ Supporto per la voce "Sara" e altre voci Kokoro
9
+ - ✅ Controllo velocità e formato audio
10
+ - ✅ API semplice e intuitiva
11
+ - ✅ Gestione automatica delle voci disponibili
12
+
13
+ ## 📋 Prerequisiti
14
+
15
+ 1. **Python 3.9 - 3.12** (⚠️ Python 3.13+ non è supportato da kokoro-tts)
16
+ - Se hai Python 3.13+, usa un ambiente virtuale con Python 3.12 o Docker
17
+ - Vedi [README_PYTHON.md](README_PYTHON.md) per dettagli
18
+ 2. **Server Kokoro TTS locale** - Vedi [COMANDI_SERVER.md](COMANDI_SERVER.md) per dettagli completi
19
+
20
+ ### 🚀 Avvio Rapido del Server
21
+
22
+ **Metodo più semplice:** Usa lo script automatico incluso:
23
+
24
+ ```bash
25
+ python start_server.py
26
+ ```
27
+
28
+ Lo script proverà automaticamente diversi metodi per avviare il server.
29
+
30
+ ### Opzioni Manuali
31
+
32
+ **Opzione 1: Docker (Consigliato)**
33
+
34
+ ```bash
35
+ # Clona il repository Kokoro-FastAPI
36
+ git clone https://github.com/remsky/Kokoro-FastAPI.git
37
+ cd Kokoro-FastAPI
38
+
39
+ # Per CPU
40
+ cd docker/cpu
41
+ docker compose up --build
42
+
43
+ # Oppure per GPU (se disponibile)
44
+ cd docker/gpu
45
+ docker compose up --build
46
+ ```
47
+
48
+ Il server sarà disponibile su `http://localhost:8880`
49
+
50
+ **Opzione 2: Server Locale Incluso (FastAPI)**
51
+
52
+ ```bash
53
+ # Installa dipendenze server
54
+ pip install -r requirements-server.txt
55
+
56
+ # Installa backend TTS (scegli uno):
57
+ pip install kokoro-tts # Richiede Python 3.9-3.12
58
+ # oppure
59
+ pip install pykokoro
60
+
61
+ # Avvia il server incluso
62
+ python kokoro_server.py
63
+ ```
64
+
65
+ **Opzione 3: uvicorn (se hai Kokoro-FastAPI come modulo)**
66
+
67
+ ```bash
68
+ pip install kokoro-fastapi
69
+ uvicorn kokoro_fastapi.app:app --host 0.0.0.0 --port 8880
70
+ ```
71
+
72
+ 📖 **Per maggiori dettagli, vedi [COMANDI_SERVER.md](COMANDI_SERVER.md)**
73
+
74
+ ## 🚀 Installazione
75
+
76
+ 1. **Clona o scarica questo progetto**
77
+
78
+ 2. **Installa le dipendenze del CLIENT Python:**
79
+
80
+ ```bash
81
+ # Per usare il client (sempre necessario)
82
+ pip install -r requirements.txt
83
+ ```
84
+
85
+ 3. **Installa il SERVER Kokoro TTS (scegli un'opzione):**
86
+
87
+ **Opzione A: Docker (Consigliato - Nessuna installazione Python)**
88
+ ```bash
89
+ # Vedi COMANDI_SERVER.md per dettagli
90
+ git clone https://github.com/remsky/Kokoro-FastAPI.git
91
+ cd Kokoro-FastAPI/docker/cpu
92
+ docker compose up --build
93
+ ```
94
+
95
+ **Opzione B: Python diretto**
96
+ ```bash
97
+ # Installa il server Kokoro
98
+ pip install -r requirements-server.txt
99
+
100
+ # Oppure direttamente:
101
+ pip install kokoro-tts-cli
102
+ ```
103
+
104
+ **Nota:** `requirements.txt` contiene solo le dipendenze del **client**. Il **server** è separato e può essere avviato con Docker (più semplice) o installato con `requirements-server.txt`.
105
+
106
+ ## 💻 Utilizzo
107
+
108
+ ### Utilizzo Base
109
+
110
+ ```python
111
+ from kokoro_tts_client import KokoroTTSClient
112
+
113
+ # Crea il client
114
+ client = KokoroTTSClient()
115
+
116
+ # Genera audio con la voce Sara
117
+ client.generate_speech_file(
118
+ text="Ciao! Sono Sara. Questo è un test.",
119
+ output_file="output/sara_test.wav",
120
+ voice="sara", # Cerca automaticamente la voce Sara
121
+ speed=1.0
122
+ )
123
+ ```
124
+
125
+ ### Esempio Completo
126
+
127
+ ```python
128
+ from kokoro_tts_client import KokoroTTSClient
129
+
130
+ # Inizializza il client
131
+ client = KokoroTTSClient(base_url="http://localhost:8880")
132
+
133
+ # Verifica connessione
134
+ if client.check_connection():
135
+ print("Server raggiungibile!")
136
+
137
+ # Mostra voci disponibili
138
+ voices = client.get_available_voices()
139
+ print(f"Voci disponibili: {voices}")
140
+
141
+ # Genera audio
142
+ audio_data = client.generate_speech(
143
+ text="Questo è un esempio di sintesi vocale.",
144
+ voice="af_sarah", # o "sara" per ricerca automatica
145
+ output_file="output/esempio.wav",
146
+ speed=1.0,
147
+ response_format="wav"
148
+ )
149
+ ```
150
+
151
+ ### Eseguire l'Esempio
152
+
153
+ ```bash
154
+ python kokoro_tts_client.py
155
+ ```
156
+
157
+ Questo eseguirà un esempio completo che:
158
+ - Verifica la connessione al server
159
+ - Mostra le voci disponibili
160
+ - Cerca la voce "Sara"
161
+ - Genera un audio di esempio
162
+
163
+ ## 🎤 Voci Disponibili
164
+
165
+ Kokoro supporta molte voci con prefissi:
166
+ - `af_*` - Voci femminili (es. `af_sarah`, `af_bella`, `af_sky`)
167
+ - `am_*` - Voci maschili (es. `am_adam`)
168
+
169
+ La funzione `find_voice()` cerca automaticamente la voce "Sara" tra quelle disponibili.
170
+
171
+ ## 📝 Parametri
172
+
173
+ ### `generate_speech()`
174
+
175
+ - `text` (str): Testo da convertire in voce
176
+ - `voice` (str): Nome della voce (default: "af_sarah")
177
+ - `output_file` (str, opzionale): Percorso del file di output
178
+ - `speed` (float): Velocità di riproduzione (default: 1.0, range consigliato: 0.5-2.0)
179
+ - `response_format` (str): Formato audio - "wav", "mp3", "opus" (default: "wav")
180
+
181
+ ## 📁 Struttura Progetto
182
+
183
+ ```
184
+ kokoro_voiceita/
185
+ ├── kokoro_tts_client.py # Cliente principale
186
+ ├── start_server.py # Script per avviare il server
187
+ ├── example_usage.py # Esempi di utilizzo
188
+ ├── config.py # File di configurazione
189
+ ├── requirements.txt # Dipendenze Python
190
+ ├── README.md # Questa documentazione
191
+ ├── COMANDI_SERVER.md # Guida dettagliata per avviare il server
192
+ └── output/ # Directory per file audio generati (creata automaticamente)
193
+ ```
194
+
195
+ ## 🔧 Risoluzione Problemi
196
+
197
+ ### Errore: "Impossibile connettersi al server"
198
+
199
+ 1. Verifica che il server Kokoro TTS sia avviato
200
+ 2. Controlla che l'URL sia corretto (default: `http://localhost:8880`)
201
+ 3. Verifica che la porta non sia bloccata da firewall
202
+
203
+ ### Voce "Sara" non trovata
204
+
205
+ - La funzione cerca automaticamente varianti come `af_sarah`, `af_sara`
206
+ - Puoi specificare direttamente l'ID della voce (es. `af_sarah`)
207
+ - Usa `get_available_voices()` per vedere tutte le voci disponibili
208
+
209
+ ### Formato MP3 non funziona
210
+
211
+ - Assicurati che FFmpeg sia installato sul sistema
212
+ - Usa "wav" come formato se hai problemi
213
+
214
+ ## 📚 Risorse
215
+
216
+ - [Kokoro TTS](https://kokorotts.net/)
217
+ - [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI)
218
+ - [Documentazione Kokoro](https://docs.azerion.ai/)
219
+
220
+ ## 📄 Licenza
221
+
222
+ Questo progetto è fornito come esempio. Verifica le licenze di Kokoro TTS per uso commerciale.
223
+
224
+ ## 🤝 Contributi
225
+
226
+ Sentiti libero di aprire issue o pull request per miglioramenti!
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "webvoice-mcp"
7
+ version = "0.2.0"
8
+ description = "MCP server for WebVoice — agent registration, chat, TTS, STT, translation, images"
9
+ readme = "webvoice_mcp/README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "EasyTaskFlow / WebVoice", email = "service@easytaskflow.app" }]
13
+ keywords = ["mcp", "webvoice", "tts", "stt", "cursor", "agents", "solana"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ ]
24
+ license-files = ["LICENSE"]
25
+ dependencies = [
26
+ "mcp>=1.6.0",
27
+ "httpx>=0.27.0",
28
+ ]
29
+
30
+ [project.scripts]
31
+ webvoice-mcp = "webvoice_mcp.server:main"
32
+
33
+ [project.urls]
34
+ Homepage = "https://webvoice.easytaskflow.app"
35
+ Documentation = "https://webvoice.easytaskflow.app/api/documentation/#mcp"
36
+ Repository = "https://github.com/easytaskflow/webvoice-mcp"
37
+ Issues = "https://github.com/easytaskflow/webvoice-mcp/issues"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["."]
41
+ include = ["webvoice_mcp*"]
42
+ exclude = ["tests*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,61 @@
1
+ """
2
+ Test per accounts.utils: cartelle media utente.
3
+ """
4
+ import os
5
+ import tempfile
6
+
7
+ from django.contrib.auth.models import User
8
+ from django.test import TestCase, override_settings
9
+
10
+ from accounts.utils import (
11
+ USER_MEDIA_SUBDIRS,
12
+ audit_all_user_media_directories,
13
+ ensure_all_users_media_directories,
14
+ ensure_user_media_directories,
15
+ list_missing_media_subdirs_for_user,
16
+ )
17
+
18
+
19
+ class EnsureUserMediaDirectoriesTests(TestCase):
20
+ def setUp(self):
21
+ self.tmp = tempfile.TemporaryDirectory()
22
+ self.addCleanup(self.tmp.cleanup)
23
+
24
+ @override_settings(MEDIA_ROOT=None)
25
+ def test_ensure_skips_without_media_root(self):
26
+ user = User.objects.create_user(username="u1", email="u1@example.com", password="x")
27
+ ensure_user_media_directories(user)
28
+ # Nessun crash
29
+
30
+ def test_ensure_creates_all_subdirs(self):
31
+ with self.settings(MEDIA_ROOT=self.tmp.name):
32
+ user = User.objects.create_user(username="u2", email="u2@example.com", password="x")
33
+ ensure_user_media_directories(user)
34
+ uid = str(user.pk)
35
+ for sub in USER_MEDIA_SUBDIRS:
36
+ path = os.path.join(self.tmp.name, sub, uid)
37
+ self.assertTrue(os.path.isdir(path), msg=path)
38
+
39
+ def test_list_missing_empty_after_ensure(self):
40
+ with self.settings(MEDIA_ROOT=self.tmp.name):
41
+ user = User.objects.create_user(username="u3", email="u3@example.com", password="x")
42
+ ensure_user_media_directories(user)
43
+ self.assertEqual(list_missing_media_subdirs_for_user(user), [])
44
+
45
+ def test_list_missing_when_no_media_root_lists_all_subdir_names(self):
46
+ with self.settings(MEDIA_ROOT=None):
47
+ user = User.objects.create_user(username="u4", email="u4@example.com", password="x")
48
+ missing = list_missing_media_subdirs_for_user(user)
49
+ self.assertEqual(set(missing), set(USER_MEDIA_SUBDIRS))
50
+
51
+ def test_audit_and_ensure_all(self):
52
+ with self.settings(MEDIA_ROOT=self.tmp.name):
53
+ User.objects.create_user(username="a", email="a@example.com", password="x")
54
+ User.objects.create_user(username="b", email="b@example.com", password="x")
55
+ r = ensure_all_users_media_directories()
56
+ self.assertEqual(r["users_processed"], 2)
57
+ audit = audit_all_user_media_directories()
58
+ self.assertTrue(audit["configured"])
59
+ self.assertEqual(audit["total_users"], 2)
60
+ self.assertEqual(audit["users_with_gaps"], 0)
61
+ self.assertEqual(audit["users_complete"], 2)
@@ -0,0 +1,155 @@
1
+ """Tests for agent registration API (stateless email OTP + API key)."""
2
+ import json
3
+ from decimal import Decimal
4
+ from unittest.mock import patch
5
+
6
+ from django.contrib.auth.models import User
7
+ from django.test import Client, TestCase, override_settings
8
+
9
+ from accounts.models import OTPCode
10
+ from api.models import APIKey
11
+
12
+
13
+ @override_settings(
14
+ SOLANA_DEPOSIT_ENABLED=True,
15
+ SOLANA_DEPOSIT_WALLET="DepositWallet1111111111111111111111111111",
16
+ )
17
+ class AgentRegistrationAPITests(TestCase):
18
+ def setUp(self):
19
+ self.client = Client()
20
+ self.json_headers = {'HTTP_ACCEPT': 'application/json'}
21
+
22
+ def _post_json(self, url, payload):
23
+ return self.client.post(
24
+ url,
25
+ data=json.dumps(payload),
26
+ content_type='application/json',
27
+ **self.json_headers,
28
+ )
29
+
30
+ @patch('accounts.views.send_otp_email', return_value=True)
31
+ def test_send_code_json(self, _mock_mail):
32
+ resp = self._post_json(
33
+ '/api/v1/auth/send-code/',
34
+ {
35
+ 'email': 'newagent@test.com',
36
+ 'accept_privacy': True,
37
+ 'accept_terms': True,
38
+ },
39
+ )
40
+ self.assertEqual(resp.status_code, 200)
41
+ data = resp.json()
42
+ self.assertTrue(data['success'])
43
+ self.assertTrue(data['is_new_user'])
44
+
45
+ @patch('accounts.views.send_otp_email', return_value=True)
46
+ def test_verify_creates_user_and_api_key_stateless(self, _mock_mail):
47
+ email = 'agent2@test.com'
48
+ self._post_json(
49
+ '/api/v1/auth/send-code/',
50
+ {'email': email, 'accept_privacy': True, 'accept_terms': True},
51
+ )
52
+ otp = OTPCode.objects.filter(email=email, used=False).latest('created_at')
53
+
54
+ resp = self._post_json(
55
+ '/api/v1/auth/verify-code/',
56
+ {
57
+ 'email': email,
58
+ 'code': otp.code,
59
+ 'accept_privacy': True,
60
+ 'accept_terms': True,
61
+ 'create_api_key': True,
62
+ 'api_key_name': 'test-agent',
63
+ },
64
+ )
65
+ self.assertEqual(resp.status_code, 200)
66
+ data = resp.json()
67
+ self.assertTrue(data['success'])
68
+ self.assertTrue(data['is_new_user'])
69
+ self.assertIn('api_key', data)
70
+ self.assertTrue(data['api_key'].startswith('wv_'))
71
+ self.assertIn('onboarding', data)
72
+ self.assertIn('solana', data['onboarding'])
73
+ self.assertTrue(data['onboarding']['solana']['available'])
74
+ self.assertIn('memo_code', data['onboarding']['solana'])
75
+ self.assertTrue(data['onboarding']['can_use_api'])
76
+
77
+ user = User.objects.get(email=email)
78
+ self.assertGreaterEqual(user.profile.credits, Decimal('20'))
79
+ self.assertEqual(APIKey.objects.filter(user=user).count(), 1)
80
+
81
+ def test_onboarding_requires_api_key(self):
82
+ resp = self.client.get(
83
+ '/api/v1/onboarding/',
84
+ HTTP_ACCEPT='application/json',
85
+ )
86
+ self.assertEqual(resp.status_code, 401)
87
+
88
+ @patch('accounts.views.send_otp_email', return_value=True)
89
+ def test_onboarding_with_api_key(self, _mock_mail):
90
+ email = 'agent3@test.com'
91
+ self._post_json(
92
+ '/api/v1/auth/send-code/',
93
+ {'email': email, 'accept_privacy': True, 'accept_terms': True},
94
+ )
95
+ otp = OTPCode.objects.filter(email=email, used=False).latest('created_at')
96
+ verify = self._post_json(
97
+ '/api/v1/auth/verify-code/',
98
+ {'email': email, 'code': otp.code, 'create_api_key': True},
99
+ ).json()
100
+ api_key = verify['api_key']
101
+
102
+ resp = self.client.get(
103
+ '/api/v1/onboarding/',
104
+ HTTP_ACCEPT='application/json',
105
+ HTTP_X_API_KEY=api_key,
106
+ )
107
+ self.assertEqual(resp.status_code, 200)
108
+ data = resp.json()
109
+ self.assertTrue(data['success'])
110
+ self.assertIn('solana', data)
111
+
112
+ @patch('accounts.views.send_otp_email', return_value=True)
113
+ def test_verify_existing_user_not_marked_new_despite_session(self, _mock_mail):
114
+ """Session is_new_user from another send-code must not flag returning users as new."""
115
+ email = 'returning@test.com'
116
+ User.objects.create_user(username='returning', email=email)
117
+ User.objects.get(email=email).set_unusable_password()
118
+
119
+ # Poison session: send-code for a different new email
120
+ self._post_json(
121
+ '/api/v1/auth/send-code/',
122
+ {'email': 'other-new@test.com', 'accept_privacy': True, 'accept_terms': True},
123
+ )
124
+ otp = OTPCode.create_code(email, '127.0.0.1', validity_minutes=15)
125
+
126
+ resp = self._post_json(
127
+ '/api/v1/auth/verify-code/',
128
+ {'email': email, 'code': otp.code, 'create_api_key': True},
129
+ )
130
+ self.assertEqual(resp.status_code, 200)
131
+ data = resp.json()
132
+ self.assertFalse(data['is_new_user'])
133
+ self.assertIn('api_key', data)
134
+
135
+ @patch('billing.solana_deposits.get_or_create_deposit_account')
136
+ @patch('accounts.views.send_otp_email', return_value=True)
137
+ def test_onboarding_survives_missing_solana_tables(self, _mock_mail, mock_memo):
138
+ from django.db.utils import ProgrammingError
139
+
140
+ mock_memo.side_effect = ProgrammingError("Table doesn't exist")
141
+ email = 'nosolana@test.com'
142
+ self._post_json(
143
+ '/api/v1/auth/send-code/',
144
+ {'email': email, 'accept_privacy': True, 'accept_terms': True},
145
+ )
146
+ otp = OTPCode.objects.filter(email=email, used=False).latest('created_at')
147
+ verify = self._post_json(
148
+ '/api/v1/auth/verify-code/',
149
+ {'email': email, 'code': otp.code, 'create_api_key': True},
150
+ )
151
+ self.assertEqual(verify.status_code, 200)
152
+ onboarding = verify.json()['onboarding']
153
+ self.assertIn('api_key', verify.json())
154
+ self.assertFalse(onboarding['solana']['available'])
155
+ self.assertTrue(onboarding['can_use_api'])