extends SceneTree ## Verifica le regole del profilo locale (nome, id stabile, blocco del 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: valori predefiniti, id generato, nome non bloccato var fresh := Profile.load_profile(TEST_PATH) check("profilo nuovo: nome predefinito e nome non bloccato", fresh["name"] == Profile.DEFAULT_NAME and fresh["locked"] == false, "nome=%s bloccato=%s" % [str(fresh["name"]), str(fresh["locked"])]) check("profilo nuovo: id del giocatore generato (32 esadecimali)", fresh["player_id"].length() == 32 and Profile.is_valid_player_id(fresh["player_id"]), "id=%s" % str(fresh["player_id"])) # 2) l'id resta lo stesso a ogni lettura (i punteggi vecchi restano collegati) var again := Profile.load_profile(TEST_PATH) check("l'id del giocatore e' stabile tra una lettura e l'altra", again["player_id"] == fresh["player_id"], "%s vs %s" % [str(again["player_id"]), str(fresh["player_id"])]) # 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("un nome scelto blocca il nome (non si richiede piu')", reloaded["locked"] == true, "bloccato=%s" % str(reloaded["locked"])) # 4) il nome scelto e' persistito anche nel file, non solo in memoria var config := ConfigFile.new() var wrote := config.load(TEST_PATH) == OK check("il blocco del nome e' scritto nel file del profilo", wrote and bool(config.get_value("player", "locked", false)), "letto=%s" % str(wrote)) # 5) profili vecchi (senza il campo locked) con nome personalizzato risultano bloccati check("profilo vecchio con nome personalizzato: nome considerato scelto", Profile.is_name_locked({"name": "Vecchio", "player_id": "x"}) == true) check("profilo vecchio con nome predefinito: nome non considerato scelto", 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 diventa quello predefinito", 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 al profilo", from_browser["name"] == "Mat" and int(from_browser["best"]) == 800 and from_browser["locked"] == true and from_browser["player_id"] == "024a956d532357c426ceb08ff86124e1", "nome=%s best=%s" % [str(from_browser["name"]), str(from_browser["best"])]) Profile.apply_web_json(from_browser, '{"n":"null","i":"corto","b":"molto","l":"1"}') check("valori non validi dal browser: nome riparato, id e punteggio intatti", from_browser["name"] == Profile.DEFAULT_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 (nome predefinito, sbloccato)", repaired["name"] == Profile.DEFAULT_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", "")) == Profile.DEFAULT_NAME 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, record e blocco migrati senza perdite", migrated.name == "Legacy" and migrated.player_id == "abcdef0123456789" and migrated.best == 850 and migrated.locked) 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)