Pacchetto Windows: test dell'avvio, workflow rilascio-windows, sezione README (TieMeasureFlow#76)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
|||||||
|
# TieMeasureFlow#76: setup.exe di TieMeasureFlow per Windows, generato al tag v*.
|
||||||
|
# Cosa fa: submodule vendor/visionsuite (deploy key), build.ps1 (uv sync Python 3.13, Tailwind,
|
||||||
|
# Nuitka --standalone, migrazioni .pyc, DLL di WeasyPrint, Inno Setup), poi il setup come
|
||||||
|
# artifact e come asset della release Gitea del tag.
|
||||||
|
# Da cosa dipende: un runner Gitea con label `windows` che giri su un host Windows (non in
|
||||||
|
# container) con Visual Studio/Build Tools (MSVC), git e node (serve a actions/checkout e a
|
||||||
|
# Tailwind); uv e Inno Setup li installa il workflow se mancano. Secret SUBMODULI_SSH_KEY
|
||||||
|
# (deploy key di sola lettura di TieVision-Core, impostato sul repo il 17/10/2026).
|
||||||
|
# Il runner `windows` e' act_runner in modalita' host su daitarn (capacity 1).
|
||||||
|
# ⚠️ upload-artifact@v3: e' la versione che usa gia' vs-native.yml in TieVision-Core su questo
|
||||||
|
# Gitea; la v4 usa un protocollo che act_runner puo' non supportare.
|
||||||
|
# ⚠️ L'asset della release usa il token del job (secrets.GITHUB_TOKEN): se la chiamata risponde
|
||||||
|
# 403 serve un token personale in un secret (es. RILASCI_TOKEN) al posto di GITHUB_TOKEN.
|
||||||
|
# ⚠️ La build da freddo e' lunga (Nuitka compila backend, visione, plotly, opencv...): timeout 180 min.
|
||||||
|
name: rilascio-windows
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v*']
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
setup-windows:
|
||||||
|
runs-on: windows
|
||||||
|
timeout-minutes: 180
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: powershell
|
||||||
|
steps:
|
||||||
|
# Checkout senza ssh-key (come ci.yml): i submodule si inizializzano a parte, con la deploy key.
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Submodule privati (deploy key in sola lettura)
|
||||||
|
env:
|
||||||
|
SUBMODULI_SSH_KEY: ${{ secrets.SUBMODULI_SSH_KEY }}
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
# cartella temporanea del job, non ~/.ssh: su un runner host e' l'account dell'utente
|
||||||
|
$ssh = Join-Path $env:TEMP ("submoduli-" + [guid]::NewGuid().ToString('N'))
|
||||||
|
New-Item -ItemType Directory -Force $ssh | Out-Null
|
||||||
|
$chiave = Join-Path $ssh 'chiave'
|
||||||
|
# LF e newline finale: OpenSSH rifiuta una chiave con CRLF
|
||||||
|
[System.IO.File]::WriteAllText($chiave, ($env:SUBMODULI_SSH_KEY.Trim() + "`n"), (New-Object System.Text.UTF8Encoding $false))
|
||||||
|
# OpenSSH su Windows rifiuta una chiave leggibile da altri utenti; (F) e non (R):
|
||||||
|
# con la sola lettura Remove-Item finale non puo' cancellarla
|
||||||
|
icacls $chiave /inheritance:r | Out-Null
|
||||||
|
icacls $chiave /grant:r "$($env:USERNAME):(F)" | Out-Null
|
||||||
|
$known = Join-Path $ssh 'known_hosts'
|
||||||
|
[System.IO.File]::WriteAllText($known, "[git.tielogic.xyz]:222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDvABW6Tvruqkf7KJhJZUiAX6NeCx0enwcNXzZbECQ8J`n", (New-Object System.Text.UTF8Encoding $false))
|
||||||
|
$env:GIT_SSH_COMMAND = "ssh -i $($chiave -replace '\\','/') -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$($known -replace '\\','/')"
|
||||||
|
try {
|
||||||
|
git submodule update --init --recursive
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "git submodule update fallito" }
|
||||||
|
} finally {
|
||||||
|
# la deploy key non resta mai sul disco del runner
|
||||||
|
Remove-Item $ssh -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: uv (se manca)
|
||||||
|
run: |
|
||||||
|
if (-not (Get-Command uv -ErrorAction SilentlyContinue)) {
|
||||||
|
irm https://astral.sh/uv/install.ps1 | iex
|
||||||
|
}
|
||||||
|
# il PATH del passo successivo
|
||||||
|
"$env:USERPROFILE\.local\bin" | Out-File -Append -Encoding utf8 $env:GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Node (se manca)
|
||||||
|
run: |
|
||||||
|
# serve a Tailwind (npx) in build.ps1
|
||||||
|
if (-not (Get-Command npx -ErrorAction SilentlyContinue)) {
|
||||||
|
winget install --id OpenJS.NodeJS.LTS -e --silent --accept-package-agreements --accept-source-agreements
|
||||||
|
}
|
||||||
|
"$env:ProgramFiles
|
||||||
|
odejs" | Out-File -Append -Encoding utf8 $env:GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Inno Setup (se manca)
|
||||||
|
run: |
|
||||||
|
$trovato = (Get-Command ISCC.exe -ErrorAction SilentlyContinue) -or
|
||||||
|
(Test-Path "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe") -or
|
||||||
|
(Test-Path "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe")
|
||||||
|
if (-not $trovato) {
|
||||||
|
winget install --id JRSoftware.InnoSetup -e --silent --accept-package-agreements --accept-source-agreements
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Build (Nuitka + Inno Setup)
|
||||||
|
# GITHUB_REF_NAME e' il tag (v0.1.0); con workflow_dispatch e' il ramo: build.ps1 rifiuta
|
||||||
|
# un nome senza numeri, quindi in quel caso si passa 0.0.0
|
||||||
|
run: |
|
||||||
|
$v = '${{ github.ref_name }}'
|
||||||
|
if ($v -notmatch '^v?\d+\.\d+\.\d+') { $v = '0.0.0' }
|
||||||
|
powershell -ExecutionPolicy Bypass -File packaging\windows\build.ps1 -Versione $v
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "build.ps1 fallito" }
|
||||||
|
|
||||||
|
- name: Setup come artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: TieMeasureFlow-setup
|
||||||
|
path: dist/TieMeasureFlow-setup-*.exe
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Asset della release Gitea (solo ai tag)
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$h = @{ Authorization = "token $env:TOKEN" }
|
||||||
|
# la release del tag: la crea il primo job che arriva, poi si riusa
|
||||||
|
try {
|
||||||
|
$rel = Invoke-RestMethod -Headers $h "$env:API/releases/tags/$env:TAG"
|
||||||
|
} catch {
|
||||||
|
$corpo = @{ tag_name = $env:TAG; name = "TieMeasureFlow $env:TAG" } | ConvertTo-Json
|
||||||
|
$rel = Invoke-RestMethod -Method Post -Headers $h -ContentType 'application/json' -Body $corpo "$env:API/releases"
|
||||||
|
}
|
||||||
|
$setup = Get-ChildItem dist\TieMeasureFlow-setup-*.exe | Select-Object -First 1
|
||||||
|
# curl.exe (incluso in Windows 10+) fa il multipart senza codice a mano
|
||||||
|
curl.exe --fail -sS -X POST -H "Authorization: token $env:TOKEN" -F "attachment=@$($setup.FullName)" "$env:API/releases/$($rel.id)/assets?name=$($setup.Name)"
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "caricamento dell'asset fallito" }
|
||||||
@@ -532,6 +532,54 @@ Nella schermata Stazioni si compilano `camera_url` (per esempio `http://host.doc
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Pacchetto Windows
|
||||||
|
|
||||||
|
Per i PC senza Docker (per esempio il PC della stazione in fiera) TieMeasureFlow si distribuisce come programma di installazione per Windows, `TieMeasureFlow-setup-<versione>.exe`. Funziona completamente offline, non richiede Docker né MySQL, e non contiene codice sorgente Python: il codice del prodotto è compilato con Nuitka. Il programma parte solo con una licenza valida, legata al computer.
|
||||||
|
|
||||||
|
### Installazione
|
||||||
|
|
||||||
|
1. Eseguire `TieMeasureFlow-setup-<versione>.exe` (serve un account amministratore; per una prova senza amministratore si può lanciare il setup con `/CURRENTUSER`). Per un'installazione silenziosa: `/VERYSILENT /SUPPRESSMSGBOXES /NORESTART`.
|
||||||
|
2. Dal menu Start aprire «Impronta di questo computer» e comunicare l'impronta a Tielogic. L'impronta viene salvata anche in `C:\ProgramData\Tielogic\licenze\impronta-tiemeasureflow.txt`.
|
||||||
|
3. Copiare il file di licenza ricevuto in `C:\ProgramData\Tielogic\licenze\tiemeasureflow.json`. La licenza non è inclusa nel setup e non va mai inserita nel repository.
|
||||||
|
4. Avviare «TieMeasureFlow» dal menu Start o dal desktop. Senza una licenza valida (mancante, scaduta, di un altro computer o di un altro prodotto) il programma mostra il motivo e l'impronta ed esce.
|
||||||
|
|
||||||
|
### Primo avvio
|
||||||
|
|
||||||
|
Il programma gira in un solo processo, senza finestra, e apre il browser. Ascolta solo sul computer locale (`127.0.0.1`); per raggiungere l'interfaccia da altri dispositivi della rete si avvia l'exe con `--host 0.0.0.0` (per esempio da un collegamento personalizzato).
|
||||||
|
|
||||||
|
| Componente | Indirizzo |
|
||||||
|
|---|---|
|
||||||
|
| Interfaccia web (Flask) | http://127.0.0.1:8780/ |
|
||||||
|
| Backend API (FastAPI) | http://127.0.0.1:8000/ |
|
||||||
|
| Worker di visione | http://127.0.0.1:8100/ |
|
||||||
|
| Camera della stazione | http://127.0.0.1:8765/ (`/camera/sources`) |
|
||||||
|
|
||||||
|
Al primo avvio vengono create la cartella dati `C:\ProgramData\Tielogic\TieMeasureFlow` e il file `config.env` con le chiavi segrete generate (`SERVER_SECRET_KEY`, `CLIENT_SECRET_KEY`, `SETUP_PASSWORD`, `DB_PASSWORD`); al riavvio vengono rilette, quindi le sessioni e le password restano valide. Il database è un file SQLite (`tmf.db`); le migrazioni si applicano da sole a ogni avvio.
|
||||||
|
|
||||||
|
Se il database non contiene ancora utenti, il browser si apre direttamente sulla pagina di setup `http://127.0.0.1:8000/api/setup`. La pagina chiede la `SETUP_PASSWORD`, che si legge in `config.env`, e permette di inizializzare il database e creare il primo amministratore (vedi «Setup Iniziale»). Terminato il setup si accede all'interfaccia su `http://127.0.0.1:8780/`.
|
||||||
|
|
||||||
|
Per le camere della stazione si compila nella schermata Stazioni `camera_url` = `http://127.0.0.1:8765` e `camera_source` (`camera:<seriale>` oppure `camera:folder`). Con `camera:folder` il servizio restituisce a rotazione le immagini che si copiano in `C:\ProgramData\Tielogic\TieMeasureFlow\camera`. Per le camere GigE/GenTL il driver (producer `.cti`) è quello installato dal produttore della camera.
|
||||||
|
|
||||||
|
Altre informazioni utili:
|
||||||
|
|
||||||
|
- I log sono in `C:\ProgramData\Tielogic\TieMeasureFlow\log\tiemeasureflow.log`.
|
||||||
|
- Una seconda esecuzione mentre il programma è già attivo apre soltanto il browser.
|
||||||
|
- Per fermare il programma si usa «Arresta TieMeasureFlow» nel menu Start (chiude il processo).
|
||||||
|
- La disinstallazione non cancella licenze, database, file caricati e `config.env`: sono dati del cliente.
|
||||||
|
- I report PDF usano le librerie grafiche incluse nel pacchetto e un browser Chrome o Edge già installato sul computer per i grafici.
|
||||||
|
|
||||||
|
### Come si costruisce
|
||||||
|
|
||||||
|
Il setup lo produce la CI Gitea (`.gitea/workflows/rilascio-windows.yml`) quando si crea un tag `v*`, su un runner con etichetta `windows`; il file compare come allegato della release. In locale, dalla radice del repository con il sottomodulo `vendor/visionsuite` inizializzato:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File packaging\windows\build.ps1 -Versione 3.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Servono `uv`, Node.js, Visual Studio con il carico C++ e Inno Setup 6. Il risultato è `dist\TieMeasureFlow-setup-3.0.0.exe`. La prima compilazione richiede decine di minuti. I test dell'avvio si lanciano con l'ambiente Python 3.13 del pacchetto: `uv run --python 3.13 --extra server --extra client --extra vision-worker --group windows --with pytest pytest packaging/windows/tests -q`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Documentazione
|
## Documentazione
|
||||||
|
|
||||||
Indice completo: [`docs/README.md`](docs/README.md).
|
Indice completo: [`docs/README.md`](docs/README.md).
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ def main(argv=None) -> int:
|
|||||||
|
|
||||||
# Si resta vivi finche' tutti i server girano; se uno muore si ferma tutto
|
# Si resta vivi finche' tutti i server girano; se uno muore si ferma tutto
|
||||||
try:
|
try:
|
||||||
while all(t.is_alive() for t in thread):
|
while thread and all(t.is_alive() for t in thread):
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
"""Test di packaging/windows/avvio.py (TieMeasureFlow#76): licenza firmata con la chiave di prova
|
||||||
|
del contratto v1, nessun server vero (avvia_server e migrazioni sono sostituiti).
|
||||||
|
|
||||||
|
Si lanciano con l'ambiente 3.13 del pacchetto (serve visionsuite.license):
|
||||||
|
UV_PROJECT_ENVIRONMENT=build/venv313 uv run --python 3.13 --frozen --extra server --extra client \
|
||||||
|
--extra vision-worker --group windows --with pytest pytest packaging/windows/tests -q
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import functools
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
from datetime import date, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||||
|
|
||||||
|
RADICE = Path(__file__).resolve().parents[3]
|
||||||
|
# vettore di prova del contratto licenza v1 (chiave finta, solo test)
|
||||||
|
SEED_TEST = base64.b64decode("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=")
|
||||||
|
PUBBLICA_TEST = base64.b64decode("A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=")
|
||||||
|
IMPRONTA = "ab" * 32
|
||||||
|
|
||||||
|
|
||||||
|
def _firma(dati: dict) -> str:
|
||||||
|
# stessa canonicalizzazione del contratto (byte firmati)
|
||||||
|
byte = json.dumps(dati, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||||
|
return base64.b64encode(Ed25519PrivateKey.from_private_bytes(SEED_TEST).sign(byte)).decode()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def avvio(monkeypatch, tmp_path):
|
||||||
|
# avvio.py non e' in un pacchetto: lo si carica dal percorso
|
||||||
|
spec = importlib.util.spec_from_file_location("avvio_tmf", RADICE / "packaging/windows/avvio.py")
|
||||||
|
m = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(m)
|
||||||
|
monkeypatch.setenv("PROGRAMDATA", str(tmp_path)) # ProgramData finta
|
||||||
|
monkeypatch.delenv("TIELOGIC_LICENZA", raising=False)
|
||||||
|
monkeypatch.setattr(os, "environ", os.environ.copy()) # le variabili impostate da avvio non escono dal test
|
||||||
|
monkeypatch.setattr(m.lic, "impronta", lambda: IMPRONTA)
|
||||||
|
# verifica reale (formato, firma, prodotto, macchina, scadenza) ma con la chiave di prova
|
||||||
|
verifica_reale = m.lic.verifica
|
||||||
|
monkeypatch.setattr(m.lic, "verifica", functools.partial(
|
||||||
|
verifica_reale, chiave_pubblica=PUBBLICA_TEST, impronta_attesa=IMPRONTA))
|
||||||
|
m.chiamate = []
|
||||||
|
monkeypatch.setattr(m, "avvia_server", lambda host, dati: m.chiamate.append((host, dati)) or [])
|
||||||
|
monkeypatch.setattr(m, "esegui_migrazioni", lambda: m.chiamate.append("migrazioni"))
|
||||||
|
m.aperti = []
|
||||||
|
monkeypatch.setattr(m.webbrowser, "open", lambda url: m.aperti.append(url))
|
||||||
|
monkeypatch.setattr(m, "_prepara_log", lambda dati=None: None)
|
||||||
|
monkeypatch.setattr(m, "_porta_aperta", lambda porta: False)
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def _scrivi_licenza(tmp_path: Path, **modifiche) -> Path:
|
||||||
|
dati = {"versione": 1, "prodotto": "tiemeasureflow", "cliente": "Prova Srl", "impronta": IMPRONTA,
|
||||||
|
"emessa": date.today().isoformat(), "scadenza": (date.today() + timedelta(days=30)).isoformat()}
|
||||||
|
dati.update(modifiche)
|
||||||
|
p = tmp_path / "Tielogic" / "licenze" / "tiemeasureflow.json"
|
||||||
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
p.write_text(json.dumps({"dati": dati, "firma": _firma(dati)}), encoding="utf-8")
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def test_licenza_mancante_non_avvia_e_scrive_impronta(avvio, capsys):
|
||||||
|
assert avvio.main(["--senza-finestre"]) == 1
|
||||||
|
assert avvio.chiamate == [] and avvio.aperti == []
|
||||||
|
assert avvio.percorso_impronta().read_text(encoding="utf-8").strip() == IMPRONTA
|
||||||
|
err = capsys.readouterr().err
|
||||||
|
assert "Licenza non trovata" in err and IMPRONTA in err and "tiemeasureflow.json" in err
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("modifica,testo", [
|
||||||
|
({"scadenza": "2020-01-01"}, "scaduta"),
|
||||||
|
({"impronta": "cd" * 32}, "altra macchina"),
|
||||||
|
({"prodotto": "tievision-studio"}, "altro prodotto"),
|
||||||
|
])
|
||||||
|
def test_licenza_non_valida_non_avvia(avvio, tmp_path, capsys, modifica, testo):
|
||||||
|
_scrivi_licenza(tmp_path, **modifica)
|
||||||
|
assert avvio.main(["--senza-finestre"]) == 1
|
||||||
|
assert avvio.chiamate == []
|
||||||
|
assert testo in capsys.readouterr().err
|
||||||
|
assert avvio.percorso_impronta().read_text(encoding="utf-8").strip() == IMPRONTA
|
||||||
|
|
||||||
|
|
||||||
|
def test_licenza_manomessa_non_avvia(avvio, tmp_path, capsys):
|
||||||
|
p = _scrivi_licenza(tmp_path)
|
||||||
|
doc = json.loads(p.read_text(encoding="utf-8"))
|
||||||
|
doc["dati"]["cliente"] = "Altro Cliente" # dati cambiati dopo la firma
|
||||||
|
p.write_text(json.dumps(doc), encoding="utf-8")
|
||||||
|
assert avvio.main(["--senza-finestre"]) == 1
|
||||||
|
assert "Firma" in capsys.readouterr().err
|
||||||
|
|
||||||
|
|
||||||
|
def test_licenza_valida_prepara_dati_e_avvia(avvio, tmp_path):
|
||||||
|
_scrivi_licenza(tmp_path)
|
||||||
|
assert avvio.main(["--senza-finestre", "--senza-browser", "--host", "0.0.0.0"]) == 0
|
||||||
|
dati = tmp_path / "Tielogic" / "TieMeasureFlow"
|
||||||
|
# prima le migrazioni, poi i server
|
||||||
|
assert avvio.chiamate == ["migrazioni", ("0.0.0.0", dati)]
|
||||||
|
assert (dati / "config.env").is_file() and (dati / "uploads").is_dir() and (dati / "camera").is_dir()
|
||||||
|
assert not avvio.percorso_impronta().exists() # con licenza valida non si scrive nulla
|
||||||
|
assert avvio.aperti == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_variabili_del_contratto(avvio, tmp_path):
|
||||||
|
_scrivi_licenza(tmp_path)
|
||||||
|
avvio.main(["--senza-finestre", "--senza-browser"])
|
||||||
|
e = os.environ
|
||||||
|
dati = tmp_path / "Tielogic" / "TieMeasureFlow"
|
||||||
|
assert e["APP_ENV"] == "production"
|
||||||
|
assert e["DB_URL"] == f"sqlite+aiosqlite:///{(dati / 'tmf.db').as_posix()}" and "\\" not in e["DB_URL"]
|
||||||
|
assert e["UPLOAD_DIR"] == str(dati / "uploads") == e["VISION_RECIPES_ROOT"]
|
||||||
|
assert e["VISION_WORKER_URL"] == "http://127.0.0.1:8100"
|
||||||
|
assert e["API_SERVER_URL"] == "http://127.0.0.1:8000"
|
||||||
|
assert e["SESSION_COOKIE_SECURE"] == "0"
|
||||||
|
for k in ("SERVER_SECRET_KEY", "CLIENT_SECRET_KEY", "SETUP_PASSWORD", "DB_PASSWORD"):
|
||||||
|
assert len(e[k]) >= 32
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_env_generato_una_volta_e_riletto(avvio, tmp_path):
|
||||||
|
dati = tmp_path / "Tielogic" / "TieMeasureFlow"
|
||||||
|
primo = avvio.carica_segreti(dati)
|
||||||
|
testo = (dati / "config.env").read_text(encoding="utf-8")
|
||||||
|
assert set(primo) == {"SERVER_SECRET_KEY", "CLIENT_SECRET_KEY", "SETUP_PASSWORD", "DB_PASSWORD"}
|
||||||
|
assert len(set(primo.values())) == 4 # segreti distinti
|
||||||
|
secondo = avvio.carica_segreti(dati)
|
||||||
|
assert secondo == primo and (dati / "config.env").read_text(encoding="utf-8") == testo
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_env_completa_i_segreti_mancanti(avvio, tmp_path):
|
||||||
|
dati = tmp_path / "Tielogic" / "TieMeasureFlow"
|
||||||
|
dati.mkdir(parents=True)
|
||||||
|
(dati / "config.env").write_text("# nota\nSETUP_PASSWORD=scelta-da-me\n", encoding="utf-8")
|
||||||
|
v = avvio.carica_segreti(dati)
|
||||||
|
assert v["SETUP_PASSWORD"] == "scelta-da-me" and len(v["SERVER_SECRET_KEY"]) >= 32
|
||||||
|
assert avvio.carica_segreti(dati) == v
|
||||||
|
|
||||||
|
|
||||||
|
def test_seconda_istanza_apre_solo_il_browser(avvio, tmp_path, monkeypatch):
|
||||||
|
_scrivi_licenza(tmp_path)
|
||||||
|
monkeypatch.setattr(avvio, "_porta_aperta", lambda porta: True)
|
||||||
|
monkeypatch.setattr(avvio, "_e_tmf", lambda: True)
|
||||||
|
assert avvio.main(["--senza-finestre"]) == 0
|
||||||
|
assert avvio.aperti == ["http://127.0.0.1:8780/"] and avvio.chiamate == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_porta_occupata_da_altro_programma(avvio, tmp_path, monkeypatch, capsys):
|
||||||
|
_scrivi_licenza(tmp_path)
|
||||||
|
monkeypatch.setattr(avvio, "_porta_aperta", lambda porta: True)
|
||||||
|
monkeypatch.setattr(avvio, "_e_tmf", lambda: False)
|
||||||
|
assert avvio.main(["--senza-finestre"]) == 2
|
||||||
|
assert "8780" in capsys.readouterr().err and avvio.chiamate == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrazione_fallita_esce_con_3(avvio, tmp_path, monkeypatch, capsys):
|
||||||
|
_scrivi_licenza(tmp_path)
|
||||||
|
|
||||||
|
def rotta():
|
||||||
|
raise RuntimeError("db bloccato")
|
||||||
|
|
||||||
|
monkeypatch.setattr(avvio, "esegui_migrazioni", rotta)
|
||||||
|
assert avvio.main(["--senza-finestre"]) == 3
|
||||||
|
assert "db bloccato" in capsys.readouterr().err
|
||||||
|
|
||||||
|
|
||||||
|
def test_impronta_si_stampa_senza_licenza(avvio, capsys):
|
||||||
|
assert avvio.main(["--impronta", "--senza-finestre"]) == 0
|
||||||
|
assert IMPRONTA in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
|
def test_nessun_utente_rileva_il_primo_avvio(avvio, tmp_path):
|
||||||
|
db = tmp_path / "t.db"
|
||||||
|
assert avvio._nessun_utente(db) is False # file assente: nessuna conclusione
|
||||||
|
con = sqlite3.connect(db)
|
||||||
|
con.execute("CREATE TABLE users (id INTEGER)")
|
||||||
|
con.commit()
|
||||||
|
assert avvio._nessun_utente(db) is True
|
||||||
|
con.execute("INSERT INTO users VALUES (1)")
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
assert avvio._nessun_utente(db) is False
|
||||||
Reference in New Issue
Block a user