- services/scores-api: microservizio (solo stdlib) con SQLite WAL, idempotenza per submission_id, limiti di frequenza e validazione; immagine Docker non-root. - nginx: reverse proxy su /api/zombie-fps/ (stessa origine del gioco, niente CORS). - scripts/leaderboard.gd: client HTTP con retry idempotente; su desktop la classifica resta disattivata senza --api=, cosi' i test non sporcano la classifica pubblica. - scripts/player_profile.gd: nome, id anonimo e miglior punteggio in user:// e in window.localStorage; il nome scelto non viene piu' richiesto; riparazione dei profili con la stringa "null". - scripts/start_screen.gd: menu con nome, GIOCA e classifica; la partita parte solo dal pulsante (via la rete di sicurezza sull'input, Invio nel campo non avvia). - scripts/main.gd, player.gd: avvio esplicito, segnale died, nota dell'invio. - tools: test_scores_api.py (42), test_leaderboard (14), test_player_profile (21), test_start_screen (10); suite esistenti adeguate all'avvio manuale. - docs/HIGHSCORES.md + docs/WEB-DEPLOY.md aggiornati.
145 lines
7.4 KiB
GDScript
145 lines
7.4 KiB
GDScript
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 😀 <b> ") == "Mat b", Profile.sanitize_name(" Ma't 😀 <b> "))
|
|
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))
|
|
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(TEST_PATH))
|
|
print("%d controlli superati, %d falliti" % [checks - failures, failures])
|
|
quit(1 if failures > 0 else 0)
|