From b8e67c3d4f074dbc7409c59f4b54304252da33a2 Mon Sep 17 00:00:00 2001 From: le king fu Date: Sun, 27 Sep 2026 08:09:01 -0400 Subject: [PATCH] fix(license): apply the second review of #359 - The idempotent-deactivation test now pins the route's other 404: license_not_found stays an error. The previous negative case was a 403 that the deactivate route never returns, so swallowing license_not_found (an unknown key shown as deactivated, the local token deleted) went unnoticed. - A test deactivates this machine and checks that the re-read activation status brings the Activate button back. Removing that re-read used to leave the suite green. - The Activate button stays disabled while the list loads (selectCanActivate), the counterpart of selectCanDeactivate: a verify answer landing after a successful activation can no longer mark as fresh a list that misses this machine. - CHANGELOG: the old /simpl-resultat address is redirected by the site since la-compagnie-maximus#153, so the "Buy" bullet now says the button opens the product page directly, in the app's language. The test comment is moved to the past tense. Proven red on the fixed tree: idempotence swallowing license_not_found 1, no status re-read after deactivation 1, Activate live during a list load 1. Refs #270 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.fr.md | 2 +- CHANGELOG.md | 2 +- src-tauri/src/commands/license_commands.rs | 14 ++++--- src/components/settings/LicenseCard.tsx | 3 +- src/hooks/useLicenseMachines.test.ts | 43 ++++++++++++++++++++++ src/hooks/useLicenseMachines.ts | 10 +++++ src/shared/purchaseUrl.test.ts | 7 ++-- 7 files changed, 69 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 821c7cb..3817ace 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -7,7 +7,7 @@ - L'activation, la désactivation et la liste des machines passent désormais par les points d'accès `/v1` du serveur de licences et nomment le produit, au lieu de passer par une redirection à chaque appel (#270). - Un refus du serveur de licences est désormais expliqué dans votre langue : clé révoquée, expirée ou introuvable, nombre maximal de machines atteint, trop de tentatives (avec le délai à attendre), serveur injoignable. Il s'affichait auparavant en anglais sous un « Activation failed » générique, et une clé révoquée apparaissait comme « Aucune machine activée » (#270). - Quand le nombre maximal de machines est atteint, la liste des machines s'ouvre d'elle-même pour en désactiver une, et le message reste affiché pendant son chargement. Désactiver une machine ne réinterroge plus le serveur, et désactiver une machine que le serveur considère déjà libérée compte désormais comme réussi (#270). -- Le bouton « Acheter » de la carte Licence ouvrait une page qui n'existe pas ; il ouvre désormais la page produit dans la langue de l'application (#270). +- Le bouton « Acheter » de la carte Licence ouvre désormais directement la page produit, dans la langue de l'application, au lieu d'une ancienne adresse qui ne fonctionne que grâce à une redirection du site (#270). ### Sécurité diff --git a/CHANGELOG.md b/CHANGELOG.md index 51ec66f..43170ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Machine activation, deactivation and the machine list now call the license server's current `/v1` endpoints and name the product explicitly, instead of going through a redirect on every call (#270). - A refusal from the license server is now explained in your language: revoked, expired or unknown key, machine limit reached, too many attempts (with the wait time), server unreachable. It used to show a generic "Activation failed" in English, and a revoked key showed up as "No machines activated" (#270). - When the machine limit is reached, the machine list opens by itself so you can deactivate one, and the message stays visible while it loads. Deactivating a machine no longer queries the server a second time, and deactivating a machine the server already considers free now counts as done (#270). -- The "Buy" button of the License card opened a page that does not exist; it now opens the product page in the app's language (#270). +- The "Buy" button of the License card now opens the product page directly, in the app's language, instead of an old address that only works through a redirect on the website (#270). ### Security diff --git a/src-tauri/src/commands/license_commands.rs b/src-tauri/src/commands/license_commands.rs index b0c603b..67cb668 100644 --- a/src-tauri/src/commands/license_commands.rs +++ b/src-tauri/src/commands/license_commands.rs @@ -1099,17 +1099,19 @@ mod license_server_tests { .await; assert_eq!(deactivate_idempotent(&server.url(), "SR-BASE-k", "machine-2").await, Ok(())); - let mut revoked = mockito::Server::new_async().await; - let _r = revoked + // The route's other 404: an unknown key must stay an error, never "deactivated" + // (the command would also delete this machine's token). Only error.code decides. + let mut unknown = mockito::Server::new_async().await; + let _u = unknown .mock("POST", "/v1/licenses/deactivate") - .with_status(403) + .with_status(404) .with_header("content-type", "application/json") - .with_body(r#"{"error":{"code":"license_revoked","message":"License has been revoked"}}"#) + .with_body(r#"{"error":{"code":"license_not_found","message":"License not found"}}"#) .create_async() .await; assert_eq!( - deactivate_idempotent(&revoked.url(), "SR-BASE-k", "machine-2").await, - Err(LicenseError::LicenseRevoked) + deactivate_idempotent(&unknown.url(), "SR-BASE-k", "machine-2").await, + Err(LicenseError::LicenseNotFound) ); } diff --git a/src/components/settings/LicenseCard.tsx b/src/components/settings/LicenseCard.tsx index 49fa87e..b8e7c2b 100644 --- a/src/components/settings/LicenseCard.tsx +++ b/src/components/settings/LicenseCard.tsx @@ -22,6 +22,7 @@ export default function LicenseCard() { toggleList, showEmptyState, canDeactivate, + canActivate, } = useLicenseMachines(hasLicense); const handleSubmit = async (e: React.FormEvent) => { @@ -156,7 +157,7 @@ export default function LicenseCard() {