# ============================================================ # Omika - Tests NON invasifs des scripts d'installation des services # ============================================================ # AUCUN changement systeme : aucune creation de service, aucune ACL posee, # aucune connexion PostgreSQL/HFSQL. Seule une reservation BRIEF de ports # loopback est faite (pour tester le module), immediatement relachee. # # Valide : # 1. Syntaxe PowerShell 5.1 (0 erreur) de TOUS les fichiers de deploiement. # 2. Import du module commun + presence des fonctions attendues. # 3. Garanties : aucun demarrage de service, aucune creation de service hors # du module, aucun port en dur, -WhatIf / -Desinstaller supportes. # 4. Fiche reelle de chaque service (nom, executable, compte par defaut). # 5. Comportement reel de la verification de port (3 cas). # ============================================================ $ErrorActionPreference = 'Stop' $deploySrc = Split-Path -Parent $PSScriptRoot $repo = Split-Path -Parent (Split-Path -Parent $deploySrc) $tests = 0 $fails = 0 function Assert([bool]$condition, [string]$label) { $script:tests++ if ($condition) { Write-Host ("PASS {0}" -f $label) -ForegroundColor Green } else { Write-Host ("FAIL {0}" -f $label) -ForegroundColor Red $script:fails++ } } $moduleCommun = Join-Path $deploySrc 'Omika.Deploy.Common.psm1' # --- 1. Syntaxe PowerShell 5.1 ---------------------------------------------- Write-Host '=== 1. Syntaxe PowerShell 5.1 (0 erreur) ===' $fichiers = @( $moduleCommun, (Join-Path $deploySrc 'Install-OmikaApi.ps1'), (Join-Path $deploySrc 'Install-OmikaWorker.ps1'), (Join-Path $deploySrc 'Install-OmikaDiagnosticsApi.ps1'), (Join-Path $deploySrc 'Install-OmikaDiagnosticsWorker.ps1'), (Join-Path $deploySrc 'Install-OmikaDiagnosticsWeb.ps1'), (Join-Path $deploySrc 'Install-OmikaLanguesApi.ps1'), (Join-Path $deploySrc 'Install-OmikaServices.ps1'), (Join-Path $deploySrc 'Test-PortDisponible.ps1'), (Join-Path $deploySrc 'New-OmikaLanguesManifeste.ps1'), (Join-Path $deploySrc 'Install-OmikaServerFiles.ps1') ) foreach ($fichier in $fichiers) { if (-not (Test-Path -LiteralPath $fichier)) { Assert $false ("Fichier present : " + (Split-Path $fichier -Leaf)) continue } $tokens = $null $errors = $null [System.Management.Automation.Language.Parser]::ParseFile($fichier, [ref]$tokens, [ref]$errors) | Out-Null $ok = (($null -eq $errors) -or ($errors.Count -eq 0)) Assert $ok ("Syntaxe OK : " + (Split-Path $fichier -Leaf)) if (-not $ok) { foreach ($e in $errors) { Write-Host (" " + $e.Message) -ForegroundColor DarkRed } } } # --- 2. Module commun -------------------------------------------------------- Write-Host '' Write-Host '=== 2. Module commun : import + fonctions exportees ===' Import-Module $moduleCommun -Force Assert $true 'Import du module sans erreur' $fonctionsAttendues = @( 'Assert-OmikaAdmin', 'Get-OmikaPaths', 'New-OmikaDirectory', 'Resolve-OmikaAccountName', 'Assert-OmikaServiceAccount', 'Test-OmikaServiceLogonRight', 'Grant-OmikaAcl', 'Get-OmikaServiceState', 'Install-OmikaService', 'Remove-OmikaService', 'Write-OmikaServiceSummary', 'Test-OmikaPortLibre' ) foreach ($nom in $fonctionsAttendues) { $existe = $null -ne (Get-Command -Name $nom -ErrorAction SilentlyContinue) Assert $existe ("Fonction exportee : $nom") } # Le controle du code retour d'icacls est la correction cle de l'ancien script. $contenuModule = Get-Content -LiteralPath $moduleCommun -Raw Assert ($contenuModule -match 'LASTEXITCODE') 'Grant-OmikaAcl controle le code retour (LASTEXITCODE)' Assert ($contenuModule -match 'icacls\.exe') 'ACL posees via icacls.exe (appel explicite)' Assert ($contenuModule -match 'SeServiceLogonRight') 'Droit « Ouvrir une session en tant que service » verifie' Assert ($contenuModule -notmatch '(?im)^\s*Start-Service\b') 'Le module ne demarre aucun service' Assert ($contenuModule -notmatch '(?im)^\s*Restart-Service\b') 'Le module ne redemarre aucun service' Assert ($contenuModule -match 'Resolve-OmikaAccountName') 'Compte normalise en ORDINATEUR\compte (forme non ambigue)' # --- Non-regression : detection d'existence d'un compte ---------------------- # Le repli ADSI ([ADSI]("WinNT://ORDINATEUR/compte,user")) ne leve AUCUNE erreur # pour un compte inexistant : il annoncait « Existe = True » a tort. Un faux # positif est le pire des resultats ici (le script creerait un service avec un # compte absent). Le repli est desormais net.exe user. Ces deux assertions # verrouillent la correction. # On ne cherche [ADSI] que dans le CODE : le module cite legitimement ADSI dans # le commentaire qui documente la raison de son retrait. $codeModule = @(Get-Content -LiteralPath $moduleCommun | Where-Object { $_ -notmatch '^\s*#' }) $adsiDansLeCode = @($codeModule | Where-Object { $_ -match '\[ADSI\]' }) Assert ($adsiDansLeCode.Count -eq 0) 'Aucun appel ADSI dans le code (source du faux positif)' $compteAbsent = Test-OmikaLocalAccount -Account '.\svc_omika_compte_absent_xyz' Assert ($compteAbsent.Existe -eq $false) 'Compte inexistant detecte ABSENT (aucun faux positif)' $comptePresent = Test-OmikaLocalAccount -Account (".\" + $env:USERNAME) Assert ($comptePresent.Existe -eq $true) ("Compte existant detecte PRESENT ({0})" -f $env:USERNAME) # --- 3. Fiche reelle de chaque service -------------------------------------- Write-Host '' Write-Host '=== 3. Un fichier par deploiement, fiche conforme ===' $servicesAttendus = @( @{ Script = 'Install-OmikaApi.ps1'; Service = 'OmikaApi'; Exe = 'Omika.Api.exe'; Compte = '\.\\svc_omika_api' } @{ Script = 'Install-OmikaWorker.ps1'; Service = 'OmikaWorker'; Exe = 'Omika.Worker.exe'; Compte = '\.\\svc_omika_worker' } @{ Script = 'Install-OmikaDiagnosticsApi.ps1'; Service = 'OmikaDiagnosticsApi'; Exe = 'Omika.Diagnostics.Api.exe'; Compte = '\.\\svc_omika_diagapi' } @{ Script = 'Install-OmikaDiagnosticsWorker.ps1'; Service = 'OmikaDiagnosticsWorker'; Exe = 'Omika.Diagnostics.Worker.exe'; Compte = '\.\\svc_omika_diag' } @{ Script = 'Install-OmikaDiagnosticsWeb.ps1'; Service = 'OmikaDiagnosticsWeb'; Exe = 'Omika.Diagnostics.Web.exe'; Compte = '\.\\svc_omika_diagweb' } @{ Script = 'Install-OmikaLanguesApi.ps1'; Service = 'OmikaLanguesApi'; Exe = 'Omika.Langues.Api.exe'; Compte = '\.\\svc_omika_langues' } ) foreach ($fiche in $servicesAttendus) { $chemin = Join-Path $deploySrc $fiche.Script $contenu = Get-Content -LiteralPath $chemin -Raw Assert ($contenu -match ('\$ServiceName\s*=\s*''' + $fiche.Service + '''')) ` ("{0} : nom de service par defaut = {1}" -f $fiche.Script, $fiche.Service) Assert ($contenu -match [regex]::Escape($fiche.Exe)) ` ("{0} : executable attendu {1}" -f $fiche.Script, $fiche.Exe) # Motif concatene en CHAINES SIMPLES : en chaine double, PowerShell # n'echappe pas avec '\' -> "\$Account" deviendrait '\' + la valeur de la # variable $Account (vide), et le motif ne correspondrait jamais. Assert ($contenu -match ('\$Account\s*=\s*''' + $fiche.Compte + '''')) ` ("{0} : compte par defaut = {1}" -f $fiche.Script, $fiche.Compte) Assert ($contenu -match 'CmdletBinding\(SupportsShouldProcess\s*=\s*\$true\)') ` ("{0} : prend en charge -WhatIf" -f $fiche.Script) Assert ($contenu -match '\$Desinstaller') ` ("{0} : prend en charge -Desinstaller (rollback)" -f $fiche.Script) Assert ($contenu -match 'Assert-OmikaServiceAccount') ` ("{0} : valide le compte de service" -f $fiche.Script) Assert ($contenu -match 'Import-Module') ` ("{0} : utilise le module commun" -f $fiche.Script) Assert ($contenu -notmatch '(?im)^\s*Start-Service\b') ` ("{0} : ne demarre aucun service" -f $fiche.Script) Assert ($contenu -notmatch '(?im)^\s*New-Service\b') ` ("{0} : ne cree pas de service directement (passe par Install-OmikaService)" -f $fiche.Script) Assert ($contenu -notmatch '(?im)^\s*Set-Service\b.*-StartupType\s+Automatic') ` ("{0} : ne force pas le demarrage automatique" -f $fiche.Script) } # --- 4. Garanties transverses ------------------------------------------------ Write-Host '' Write-Host '=== 4. Garanties transverses ===' # Aucun port en dur : le port se decide contre le plan Apache (doc §6bis). # On ne cherche PAS le simple mot « 5099 » (il apparait legitimement dans les # exemples d'usage et dans le test de port) mais les deux formes qui # constitueraient un CHOIX code en dur : une URL ASPNETCORE_URLS et une valeur # par defaut de parametre. $portEnDur = @() foreach ($fichier in (Get-ChildItem -LiteralPath $deploySrc -Filter *.ps1 -Recurse)) { $contenu = Get-Content -LiteralPath $fichier.FullName -Raw if ($contenu -match '127\.0\.0\.1:5099') { $portEnDur += ("{0} (ASPNETCORE_URLS en dur)" -f $fichier.Name) } # 0 est autorise : il signifie « aucun port fourni », ce n'est pas un choix. elseif ($contenu -match '(?im)^\s*\[int\]\s*\$Port\w*\s*=\s*[1-9]\d*') { $portEnDur += ("{0} (valeur par defaut de -Port)" -f $fichier.Name) } } Assert ($portEnDur.Count -eq 0) ("Aucun port code en dur dans infra\deploy ({0} fichier(s))" -f $portEnDur.Count) if ($portEnDur.Count -gt 0) { Write-Host (" " + ($portEnDur -join ', ')) -ForegroundColor DarkRed } # L'orchestrateur appelle bien les 6 scripts dedies. $orchestrateur = Get-Content -LiteralPath (Join-Path $deploySrc 'Install-OmikaServices.ps1') -Raw foreach ($fiche in $servicesAttendus) { Assert ($orchestrateur -match [regex]::Escape($fiche.Script)) ` ("Orchestrateur : appelle {0}" -f $fiche.Script) } Assert ($orchestrateur -match 'SupportsShouldProcess') 'Orchestrateur : prend en charge -WhatIf' Assert ($orchestrateur -match 'InclureLangues') 'Orchestrateur : service Langues optionnel (-InclureLangues)' Assert ($orchestrateur -match 'SansDiagnosticsApi') 'Orchestrateur : API centrale optionnelle (-SansDiagnosticsApi)' # --- 5. Comportement reel de la verification de port ------------------------ Write-Host '' Write-Host '=== 5. Verification de port : 3 cas reels ===' # Normalisation du compte (forme '.\' -> 'ORDINATEUR\compte'). $normalise = Resolve-OmikaAccountName -Account '.\svc_omika_api' Assert ($normalise -eq ('{0}\svc_omika_api' -f $env:COMPUTERNAME)) "Compte normalise : $normalise" # A. Port libre. $verdictLibre = Test-OmikaPortLibre -Port 5099 -VerifierPlagesReservees:$false Assert ($verdictLibre.Libre) ("Port 5099 detecte DISPONIBLE" + $(if ($verdictLibre.Libre) { '' } else { " (raison : $($verdictLibre.Raisons))" })) # B. Port reellement occupe. $verrou = $null $verdictOccupe = $null try { $verrou = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 5123) $verrou.Start() $verdictOccupe = Test-OmikaPortLibre -Port 5123 -VerifierPlagesReservees:$false } finally { if ($verrou) { try { $verrou.Stop() } catch { } } } Assert (($null -ne $verdictOccupe) -and (-not $verdictOccupe.Libre)) 'Port 5123 occupe detecte INDISPONIBLE' # C. Port dans la plage ephemere. $verdictEphemere = Test-OmikaPortLibre -Port 50000 -VerifierPlagesReservees:$false Assert (-not $verdictEphemere.Libre) 'Port 50000 (plage ephemere) detecte INDISPONIBLE' Assert ($verdictEphemere.Raisons -match 'EPHEMERE') 'La raison invoquee mentionne la plage ephemere' # --- 6. Manifeste des packs de langue : generation reelle -------------------- # Le manifeste contient deux champs MESURES (tailleOctets, sha256) : une valeur # inventee ne se voit que sur le telephone, ou le pack est refuse. On verifie # donc la generation contre une source independante (Get-FileHash / FileInfo). Write-Host '' Write-Host '=== 6. Manifeste des packs de langue : generation reelle ===' $generateur = Join-Path $deploySrc 'New-OmikaLanguesManifeste.ps1' Assert (Test-Path -LiteralPath $generateur) 'Generateur de manifeste present' $bac = Join-Path ([System.IO.Path]::GetTempPath()) ('omika-manifeste-' + [guid]::NewGuid().ToString('N').Substring(0, 8)) try { $null = New-Item -ItemType Directory -Path (Join-Path $bac 'es\versFrancais') -Force $null = New-Item -ItemType Directory -Path (Join-Path $bac 'es\depuisFrancais') -Force [System.IO.File]::WriteAllBytes((Join-Path $bac 'es\versFrancais\encoder.onnx'), (New-Object byte[] 4096)) [System.IO.File]::WriteAllBytes((Join-Path $bac 'es\versFrancais\vocab.spm'), (New-Object byte[] 256)) [System.IO.File]::WriteAllBytes((Join-Path $bac 'es\depuisFrancais\encoder.onnx'), (New-Object byte[] 4096)) # Fichier inutile volontairement livre : exclu du chargement par l'application, # mais telecharge par chaque telephone s'il est declare. Doit etre signale. [System.IO.File]::WriteAllBytes((Join-Path $bac 'es\versFrancais\decoder_with_past_model.onnx'), (New-Object byte[] 512)) $sortieGeneration = (& $generateur -RootPathLangues $bac 6>&1 | Out-String) $manifeste = Join-Path $bac 'manifest.json' Assert (($LASTEXITCODE -eq 0) -and (Test-Path -LiteralPath $manifeste)) 'Generation : manifeste ecrit (code 0)' Assert ($sortieGeneration -match 'past_key_values') 'Generation : decodeur « _with_past » signale (poids mobile inutile)' $json = Get-Content -LiteralPath $manifeste -Raw -Encoding UTF8 | ConvertFrom-Json Assert (@($json.packs).Count -eq 1) 'Manifeste : 1 pack' Assert (@($json.packs[0].fichiers).Count -eq 4) 'Manifeste : 4 fichiers declares' $ecarts = 0 foreach ($pack in $json.packs) { foreach ($fichier in $pack.fichiers) { $chemin = Join-Path (Join-Path (Join-Path $bac $pack.code) $fichier.sens) $fichier.nomFichier if ((Get-FileHash -LiteralPath $chemin -Algorithm SHA256).Hash.ToLowerInvariant() -ne $fichier.sha256) { $ecarts++ } if ((Get-Item -LiteralPath $chemin).Length -ne $fichier.tailleOctets) { $ecarts++ } } } Assert ($ecarts -eq 0) 'Manifeste : sha256 et tailles conformes (source independante)' $octetsManifeste = [System.IO.File]::ReadAllBytes($manifeste) Assert (-not ($octetsManifeste[0] -eq 0xEF -and $octetsManifeste[1] -eq 0xBB -and $octetsManifeste[2] -eq 0xBF)) ` 'Manifeste : UTF-8 sans BOM (fichier de donnees)' $null = & $generateur -RootPathLangues $bac -Verifier 6>&1 Assert ($LASTEXITCODE -eq 0) 'Verification : manifeste conforme (code 0)' # Un modele remplace APRES generation doit etre detecte : c'est le controle # qui evite un pack silencieusement refuse sur le telephone. [System.IO.File]::WriteAllBytes((Join-Path $bac 'es\versFrancais\vocab.spm'), (New-Object byte[] 300)) $sortieManifeste = (& $generateur -RootPathLangues $bac -Verifier 6>&1 | Out-String) Assert ($LASTEXITCODE -eq 1) 'Verification : modele modifie detecte (code 1)' Assert ($sortieManifeste -match 'empreinte differente') 'Verification : ecart d''empreinte annonce' # Le mobile compare le sens de facon sensible a la casse : un sens a la # mauvaise casse ferait refuser le pack sans aucune erreur cote serveur. $casse = Join-Path $bac 'manifest-casse.json' (Get-Content -LiteralPath $manifeste -Raw -Encoding UTF8).Replace('"sens": "versFrancais"', '"sens": "VersFrancais"') | Set-Content -LiteralPath $casse -Encoding UTF8 $null = & $generateur -RootPathLangues $bac -Sortie $casse -Verifier 6>&1 Assert ($LASTEXITCODE -eq 1) 'Verification : sens a la mauvaise casse detecte (refus mobile)' Remove-Item -LiteralPath $casse -Force -ErrorAction SilentlyContinue # --- Cohabitation avec le script d'export --------------------------------- # Le dossier des packs peut contenir le cache HuggingFace du script d'export # (.hf-cache, des milliers de fichiers) et la sauvegarde du manifeste : ni # l'un ni l'autre n'est un modele, et les signaler noierait le vrai controle # sous des faux positifs. $null = New-Item -ItemType Directory -Path (Join-Path $bac '.hf-cache\hub\models--Helsinki-NLP--opus-mt-es-fr') -Force [System.IO.File]::WriteAllBytes((Join-Path $bac '.hf-cache\hub\models--Helsinki-NLP--opus-mt-es-fr\blob-pytorch'), (New-Object byte[] 128)) # Deuxieme ecriture : le generateur conserve manifest.json.bak $null = & $generateur -RootPathLangues $bac 6>&1 Assert (Test-Path -LiteralPath (Join-Path $bac 'manifest.json.bak')) 'Regeneration : sauvegarde manifest.json.bak ecrite' $sortieTechnique = (& $generateur -RootPathLangues $bac -Verifier 6>&1 | Out-String) Assert ($LASTEXITCODE -eq 0) 'Cohabitation : verification conforme malgre .hf-cache et .bak' Assert ($sortieTechnique -notmatch 'hf-cache') 'Cohabitation : .hf-cache jamais signale comme fichier non declare' Assert ($sortieTechnique -notmatch 'manifest\.json\.bak') 'Cohabitation : la sauvegarde .bak n''est pas un faux positif' } finally { if (Test-Path -LiteralPath $bac) { Remove-Item -LiteralPath $bac -Recurse -Force -ErrorAction SilentlyContinue } } # --- 6bis. Manifeste : poids partages d'un modele familial (commun/) --------- # Un modele familial range ses ~1,15 Go dans /commun/ et ne laisse qu'un # modele.json par sens. Si le generateur traitait « commun » comme un sens # inconnu, il l'ignorerait : le telephone ne pourrait JAMAIS telecharger les # poids, et le pack serait declare mais inutilisable. Write-Host '' Write-Host '=== 6bis. Manifeste : poids partages (commun/) ===' $bacFamille = Join-Path ([System.IO.Path]::GetTempPath()) ('omika-manifeste-famille-' + [guid]::NewGuid().ToString('N').Substring(0, 8)) try { $null = New-Item -ItemType Directory -Path (Join-Path $bacFamille 'pt\commun') -Force $null = New-Item -ItemType Directory -Path (Join-Path $bacFamille 'pt\versFrancais') -Force $null = New-Item -ItemType Directory -Path (Join-Path $bacFamille 'pt\depuisFrancais') -Force [System.IO.File]::WriteAllBytes((Join-Path $bacFamille 'pt\commun\encoder_model.onnx'), (New-Object byte[] 2048)) [System.IO.File]::WriteAllBytes((Join-Path $bacFamille 'pt\commun\source.spm'), (New-Object byte[] 128)) [System.IO.File]::WriteAllText((Join-Path $bacFamille 'pt\versFrancais\modele.json'), '{ "version": 1, "jetonCible": ">>fra<<" }') [System.IO.File]::WriteAllText((Join-Path $bacFamille 'pt\depuisFrancais\modele.json'), '{ "version": 1, "jetonCible": ">>por<<" }') $sortieFamille = (& $generateur -RootPathLangues $bacFamille 6>&1 | Out-String) $manifesteFamille = Join-Path $bacFamille 'manifest.json' Assert (($LASTEXITCODE -eq 0) -and (Test-Path -LiteralPath $manifesteFamille)) 'Manifeste familial : ecrit (code 0)' Assert ($sortieFamille -notmatch 'sens inconnu') 'Manifeste familial : commun/ n''est pas traite comme un sens inconnu' $jsonFamille = Get-Content -LiteralPath $manifesteFamille -Raw -Encoding UTF8 | ConvertFrom-Json $sensFamille = @($jsonFamille.packs[0].fichiers | ForEach-Object { $_.sens } | Sort-Object -Unique) Assert ($sensFamille -contains 'commun') 'Manifeste familial : le dossier commun est declare' Assert ($sensFamille -contains 'versFrancais' -and $sensFamille -contains 'depuisFrancais') ` 'Manifeste familial : les deux sens (modele.json) sont declares' Assert ((@($jsonFamille.packs[0].fichiers | Where-Object { $_.sens -eq 'commun' })).Count -eq 2) ` 'Manifeste familial : les poids sont declares une seule fois, sous commun' $null = & $generateur -RootPathLangues $bacFamille -Verifier 6>&1 Assert ($LASTEXITCODE -eq 0) 'Manifeste familial : verification conforme (code 0)' } finally { if (Test-Path -LiteralPath $bacFamille) { Remove-Item -LiteralPath $bacFamille -Recurse -Force -ErrorAction SilentlyContinue } } # --- Resultat ---------------------------------------------------------------- Write-Host '' Write-Host ("=== RESULTAT : {0} tests, {1} echec(s) ===" -f $tests, $fails) if ($fails -gt 0) { exit 1 } else { exit 0 }