feat(billing): service API-key generation + matching

Add _match_api_key() class method to fusion.billing.service, with a
TDD test suite (TestServiceApiKey) covering key generation, hash storage,
positive match, and rejection of bad/inactive keys. Also fix
fcb_test_on_trial.sh to use --http-port 8070, as Odoo 19 forces
http_spawn() even under --no-http when --test-enable is set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-27 02:44:30 -04:00
parent 032b10752e
commit a46e31e710
5 changed files with 44 additions and 3 deletions

View File

@@ -81,8 +81,15 @@ Odoo content-hashes the compiled bundle URL (`/web/assets/<hash>/...`). When CSS
- **fusion_clock** is currently being modified in Cursor — always read files fresh before editing, don't assume you know the current state
## Workflow
- Local dev: `docker exec odoo-dev-app odoo -d fusion-dev -u <module> --stop-after-init`
- Local URL: http://localhost:8069
- Local dev: `docker exec odoo-modsdev-app odoo -d fusion-dev -u <module> --stop-after-init`
- Local URL: http://localhost:8082
- **Running module tests requires ephemeral ports.** The dev container's main Odoo process holds 8069 and 8072; a `docker exec ... odoo --test-enable` will die with `Address already in use` unless you also pass `--http-port=0 --gevent-port=0`. This is because Odoo 19 forces `http_spawn()` when `--test-enable` is set, even when `--no-http` is passed. Canonical test invocation:
```bash
docker exec odoo-modsdev-app odoo -d fusion-dev --test-enable --test-tags /<module> \
-u <module> --stop-after-init --http-port=0 --gevent-port=0 2>&1 | tail -60
```
- **`fusion_centralize_billing` tests run on odoo-trial (VM 316).** Local dev is Community and cannot install this module. Use `bash scripts/fcb_test_on_trial.sh` from the repo root. The script uses `--http-port 8070` to avoid the port 8069 conflict with the live odoo-trial-app container. Pass = `FCB_EXIT=0`. Takes ~1-2 min.
- **Python deps not bundled with `odoo:19` image:** `user_agents` (used by `fusion_login_audit`), and likely others. Install ephemerally with `docker exec -u 0 odoo-modsdev-app pip install <pkg> --break-system-packages`. The install is LOST when the container is recreated (e.g. `docker compose up -d` after a compose edit). When this happens, the symptom is `ModuleNotFoundError` deep in the auth or report code. Re-run the pip install. A persistent fix would be a custom Dockerfile or a startup hook on the compose service — not done yet.
- Test before deploying. Edit existing files — don't create unnecessary new ones.
## PDF Preview — Prefer fusion_pdf_preview Over Downloads/New-Tab

View File

@@ -54,3 +54,11 @@ class FusionBillingService(models.Model):
raw = secrets.token_urlsafe(32)
self.api_key_hash = hashlib.sha256(raw.encode()).hexdigest()
return raw
@api.model
def _match_api_key(self, raw_key):
"""Return the active service whose stored hash matches raw_key, else empty recordset."""
if not raw_key:
return self.browse()
key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
return self.search([('api_key_hash', '=', key_hash), ('active', '=', True)], limit=1)

View File

@@ -0,0 +1 @@
from . import test_identity

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestServiceApiKey(TransactionCase):
def setUp(self):
super().setUp()
self.Service = self.env['fusion.billing.service'].sudo()
self.service = self.Service.create({'name': 'NexaCloud', 'code': 'nexacloud'})
def test_generate_and_match_api_key(self):
raw = self.service.action_generate_api_key()
self.assertTrue(raw and len(raw) >= 20)
self.assertTrue(self.service.api_key_hash)
self.assertNotEqual(raw, self.service.api_key_hash) # only the hash is stored
matched = self.Service._match_api_key(raw)
self.assertEqual(matched, self.service)
def test_match_api_key_rejects_unknown_and_inactive(self):
raw = self.service.action_generate_api_key()
self.assertFalse(self.Service._match_api_key('nope-not-a-key'))
self.service.active = False
self.assertFalse(self.Service._match_api_key(raw))

View File

@@ -23,5 +23,5 @@ ssh -o ConnectTimeout=40 "${PVE}" "qm guest exec ${VMID} --timeout 90 -- bash -l
2>&1 | sed -n 's/.*"out-data" : "\(.*\)",/\1/p' | sed 's/\\n/\n/g'
echo ">> upgrade + test on Enterprise 19 (db=trial, --no-http)"
ssh -o ConnectTimeout=40 "${PVE}" "qm guest exec ${VMID} --timeout 600 -- bash -lc 'docker exec odoo-trial-app odoo -d trial -u ${MODULE} --no-http --workers 0 --test-enable --test-tags /${MODULE} --stop-after-init >/tmp/fcb_test.log 2>&1; echo FCB_EXIT=\$?; grep -iE \"FAIL|ERROR|tested in|Ran |assert\" /tmp/fcb_test.log | grep -viE \"fusion_plating|fusion_tasks|not installable|not loaded\" | tail -30'" \
ssh -o ConnectTimeout=40 "${PVE}" "qm guest exec ${VMID} --timeout 600 -- bash -lc 'docker exec odoo-trial-app odoo -d trial -u ${MODULE} --no-http --http-port 8070 --workers 0 --test-enable --test-tags /${MODULE} --stop-after-init >/tmp/fcb_test.log 2>&1; echo FCB_EXIT=\$?; grep -iE \"FAIL|ERROR|tested in|Ran |assert\" /tmp/fcb_test.log | grep -viE \"fusion_plating|fusion_tasks|not installable|not loaded\" | tail -30'" \
2>&1 | sed -n 's/.*"out-data" : "\(.*\)",/\1/p' | sed 's/\\n/\n/g'