extends SceneTree ## Verifica le regole del profilo locale in modalita' arcade/kiosk (nome mai dato per ## scontato, id derivato dal nome, miglior punteggio per nome) su un file temporaneo, ## senza toccare il profilo reale del gioco. ## Avviare con: --headless --path . --script res://tools/test_player_profile.gd const Profile = preload("res://scripts/player_profile.gd") const TEST_PATH := "user://test_profile_check.cfg" var failures := 0 var checks := 0 func _initialize() -> void: call_deferred("run_test") func check(label: String, condition: bool, detail: String = "") -> void: checks += 1 if condition: print("PASS: " + label) else: failures += 1 print("FAIL: %s %s" % [label, detail]) func run_test() -> void: DirAccess.remove_absolute(ProjectSettings.globalize_path(TEST_PATH)) # 1) file assente: nessun nome e nessun id dati per scontato var fresh := Profile.load_profile(TEST_PATH) check("profilo nuovo: campo del nome libero (nessun nome presunto)", fresh["name"] == "" and fresh["locked"] == false, "nome=%s bloccato=%s" % [str(fresh["name"]), str(fresh["locked"])]) check("profilo nuovo: nessun id assegnato prima del nome inserito", str(fresh["player_id"]) == "", "id=%s" % str(fresh["player_id"])) # 2) identita' derivata dal nome della partita: deterministica e valida per il server var id_a := Profile.player_id_for_name("Mat") var id_b := Profile.player_id_for_name("Mat") check("l'id derivato dal nome e' stabile e accettato dal server", id_a == id_b and id_a.length() == 16 and Profile.is_valid_player_id(id_a), "id=%s vs %s" % [id_a, id_b]) check("nomi diversi = identita' diverse", Profile.player_id_for_name("Lu") != id_a, "%s vs %s" % [str(Profile.player_id_for_name("Lu")), id_a]) check("nome vuoto: id di ripiego generato e valido", Profile.is_valid_player_id(Profile.player_id_for_name("")), "id=%s" % Profile.player_id_for_name("")) # 3) nome scelto e punteggio migliore salvati e riletti var saved := fresh.duplicate() saved["name"] = "Mat" saved["best"] = 300 Profile.save_profile(saved, TEST_PATH) var reloaded := Profile.load_profile(TEST_PATH) check("nome e miglior punteggio sopravvivono alla riscrittura", reloaded["name"] == "Mat" and int(reloaded["best"]) == 300, "nome=%s best=%s" % [str(reloaded["name"]), str(reloaded["best"])]) check("regola arcade: il nome scelto NON blocca, viene richiesto di nuovo", reloaded["locked"] == false, "bloccato=%s" % str(reloaded["locked"])) check("il miglior punteggio e' registrato per nome (record per persona)", Profile.best_for_name(reloaded, "Mat") == 300 and Profile.best_for_name(reloaded, "Lu") == 0, "mat=%s lu=%s" % [str(Profile.best_for_name(reloaded, "Mat")), str(Profile.best_for_name(reloaded, "Lu"))]) check("game over: profilo sbloccato e nome liberato per la partita nuova", Profile.unlock_name(reloaded)["locked"] == false and str(reloaded["name"]) == "", "nome=%s bloccato=%s" % [str(reloaded["name"]), str(reloaded["locked"])]) # 4) nome, record per nome e sblocco sono scritti nel file, non solo in memoria var config := ConfigFile.new() var wrote := config.load(TEST_PATH) == OK var stored_bests: Variant = config.get_value("player", "bests", {}) check("il file conserva il record del nome e il nome non bloccato", wrote and bool(config.get_value("player", "locked", true)) == false and stored_bests is Dictionary and int(stored_bests.get("Mat", 0)) == 300, "letto=%s bests=%s" % [str(wrote), str(stored_bests)]) # 5) i profili delle versioni precedenti arrivano "bloccati": la regola arcade li ignora check("profilo vecchio con nome personalizzato: comunque non bloccato", Profile.is_name_locked({"name": "Vecchio", "player_id": "x", "locked": true}) == false) check("profilo nuovo: il nome non e' bloccato", Profile.is_name_locked({"name": Profile.DEFAULT_NAME, "player_id": "x"}) == false) # 6) pulizia del nome secondo le regole del server check("il nome viene ripulito da apici, emoji e simboli", Profile.sanitize_name(" Ma't 😀 ") == "Mat b", Profile.sanitize_name(" Ma't 😀 ")) check("il nome vuoto resta vuoto per il campo, predefinito solo lato server", Profile.sanitize_name(" ") == Profile.DEFAULT_NAME, Profile.sanitize_name(" ")) check("il nome e' limitato a 16 caratteri", Profile.sanitize_name("abcdefghijklmnopqrstuvwxyz").length() == Profile.NAME_MAX, Profile.sanitize_name("abcdefghijklmnopqrstuvwxyz")) check("le lettere accentate sono conservate", Profile.sanitize_name("Città àèìòù") == "Città àèìòù", Profile.sanitize_name("Città àèìòù")) # 7) uuid di partita: forma valida e sempre diverso (idempotenza lato server) var uuid_a := Profile.new_submission_id() var uuid_b := Profile.new_submission_id() var parts := uuid_a.split("-") check("l'identificativo di partita e' un uuid v4", parts.size() == 5 and uuid_a.length() == 36 and uuid_a[14] == "4" and "89ab".contains(uuid_a[19]) and uuid_a != uuid_b, uuid_a) # 8) testo per JavaScript: apici e barre non rompono il codice check("il testo per localStorage e' messo in sicurezza", Profile.js_escape("a'b\\c\nd") == "a\\'b\\\\c d", Profile.js_escape("a'b\\c\nd")) # 9) id del giocatore: le stesse regole del server check("l'id del giocatore viene validato come il server", Profile.is_valid_player_id("abcdef0123456789") and Profile.is_valid_player_id("player-offline-0001") and not Profile.is_valid_player_id("corto") and not Profile.is_valid_player_id("con spazio qui") and not Profile.is_valid_player_id("apostrofo'qui")) # 10) valori letti dal browser: quelli assenti NON devono diventare la stringa "null" var from_browser := {"name": Profile.DEFAULT_NAME, "player_id": "abcdef0123456789", "best": 0, "locked": false} Profile.apply_web_json(from_browser, '{"n":null,"i":null,"b":null,"l":null}') check("valori assenti nel browser non sporcano il profilo (niente \"null\")", from_browser["name"] == Profile.DEFAULT_NAME and from_browser["locked"] == false and from_browser["player_id"] == "abcdef0123456789", "nome=%s bloccato=%s" % [str(from_browser["name"]), str(from_browser["locked"])]) Profile.apply_web_json(from_browser, '{"n":"Mat","i":"024a956d532357c426ceb08ff86124e1","b":"800","l":"1"}') check("i valori del browser vengono applicati, il nome resta libero (regola arcade)", from_browser["name"] == "Mat" and int(from_browser["best"]) == 800 and from_browser["locked"] == false and from_browser["player_id"] == "024a956d532357c426ceb08ff86124e1", "nome=%s best=%s bloccato=%s" % [str(from_browser["name"]), str(from_browser["best"]), str(from_browser["locked"])]) Profile.apply_web_json(from_browser, '{"n":"null","i":"corto","b":"molto","l":"1"}') check("valori non validi dal browser: campo del nome liberato, id e punteggio intatti", from_browser["name"] == "" and from_browser["locked"] == false and from_browser["player_id"] == "024a956d532357c426ceb08ff86124e1" and int(from_browser["best"]) == 800, "nome=%s id=%s best=%s bloccato=%s" % [str(from_browser["name"]), str(from_browser["player_id"]), str(from_browser["best"]), str(from_browser["locked"])]) check("testo di un valore nullo: stringa vuota, non \"null\"", Profile.text_of(null) == "" and Profile.text_of("x") == "x", Profile.text_of(null)) # 11) riparazione automatica: un profilo sporcato con la stringa "null" # (bug di una versione precedente) torna al nome predefinito e si sblocca. var junk_path := "user://test_profile_junk.cfg" var junk := ConfigFile.new() junk.set_value("player", "name", "null") junk.set_value("player", "player_id", "abcdef0123456789") junk.set_value("player", "best", 500) junk.set_value("player", "locked", true) junk.save(junk_path) var repaired := Profile.load_profile(junk_path) check("un profilo con nome \"null\" viene riparato (campo libero, sbloccato)", repaired["name"] == "" and repaired["locked"] == false and int(repaired["best"]) == 500, "nome=%s bloccato=%s best=%s" % [str(repaired["name"]), str(repaired["locked"]), str(repaired["best"])]) var junk_after := ConfigFile.new() junk_after.load(junk_path) check("la riparazione viene riscritta nel file", str(junk_after.get_value("player", "name", "x")) == "" and bool(junk_after.get_value("player", "locked", true)) == false, "%s / %s" % [str(junk_after.get_value("player", "name", "")), str(junk_after.get_value("player", "locked", true))]) DirAccess.remove_absolute(ProjectSettings.globalize_path(junk_path)) # 12) Native rename migration: temporary files only, old source is read-only. var legacy_path := "user://test_legacy_profile.cfg" var migrated_path := "user://test_migrated_profile.cfg" DirAccess.remove_absolute(ProjectSettings.globalize_path(migrated_path)) Profile.save_profile({"name": "Legacy", "player_id": "abcdef0123456789", "best": 850, "locked": true}, legacy_path) var old_hash := FileAccess.get_sha256(legacy_path) var migrated := Profile.load_profile(migrated_path, legacy_path) check("rinomina: nome, id e record migrati senza perdite (nome libero)", migrated.name == "Legacy" and migrated.player_id == "abcdef0123456789" and int(migrated.best) == 850 and migrated.locked == false) check("rinomina: il vecchio file rimane byte-identico", FileAccess.get_sha256(legacy_path) == old_hash) check("rinomina: il profilo viene scritto nella nuova posizione", FileAccess.file_exists(migrated_path)) migrated.name = "Attuale" migrated.best = 950 Profile.save_profile(migrated, migrated_path) var preferred := Profile.load_profile(migrated_path, legacy_path) check("rinomina: un profilo nuovo esistente prevale sul vecchio", preferred.name == "Attuale" and preferred.best == 950) check("rinomina: percorso legacy risolto accanto al nuovo user data", Profile.legacy_profile_path().ends_with("/Zombie Room FPS/zombie_fps_profile.cfg")) DirAccess.remove_absolute(ProjectSettings.globalize_path(legacy_path)) DirAccess.remove_absolute(ProjectSettings.globalize_path(migrated_path)) DirAccess.remove_absolute(ProjectSettings.globalize_path(TEST_PATH)) print("%d controlli superati, %d falliti" % [checks - failures, failures]) quit(1 if failures > 0 else 0)