Volver al índice

src/main/java/ar/com/companeros/horror/HorrorSpeechTests.java

package ar.com.companeros.horror;

import ar.com.companeros.Companeros;
import java.io.IOException;
import java.nio.file.*;
import java.util.UUID;
import net.minecraft.gametest.framework.*;
import net.minecraftforge.gametest.GameTestHolder;
import net.minecraftforge.gametest.PrefixGameTestTemplate;

/** Fronteras de voz: ni texto libre ni respuestas HTTP obtienen rutas o permisos. */
@GameTestHolder(Companeros.ID)
@PrefixGameTestTemplate(false)
public final class HorrorSpeechTests {
    @GameTest(template = "empty", timeoutTicks = 20)
    public void voiceEndpointsAndTextCannotSmuggleOperations(GameTestHelper helper) {
        for (String endpoint : new String[]{"file:///C:/secret", "http://example.com/speech", "https://user:password@example.com/speech", "https://example.com/speech#fragment"}) {
            try { HorrorSpeech.permittedEndpoint(endpoint); helper.fail("Se aceptó una URL de voz insegura"); }
            catch (IllegalArgumentException expected) { }
        }
        helper.assertTrue(HorrorSpeech.permittedEndpoint("http://127.0.0.1:8775/speech").getHost().equals("127.0.0.1")
            && HorrorSpeech.permittedEndpoint("https://voice.example/speech").getScheme().equals("https")
            && HorrorSpeech.validText("La piedra todavía me recuerda.") && !HorrorSpeech.validText("x".repeat(321))
            && !HorrorSpeech.validText("voz\u0000oculta"), "El operador elige el servicio; el texto queda acotado sin controles ocultos");
        helper.succeed();
    }

    @GameTest(template = "empty", timeoutTicks = 20)
    public void generatedAudioRequiresVorbisAndPacketOwnsItsBytes(GameTestHelper helper) {
        try (var stream = getClass().getResourceAsStream("/assets/companeros/sounds/parasite_voice.ogg")) {
            helper.assertTrue(stream != null, "Existe una voz propia empaquetada");
            byte[] audio = stream.readAllBytes();
            helper.assertTrue(HorrorSpeech.validAudio(audio), "La voz generada respeta el formato y límite del canal");
            var packet = new HorrorNetwork.Speech(UUID.randomUUID(), 0, 64, 0, audio);
            audio[0] = 0; byte[] escaped = packet.audio(); escaped[1] = 0;
            helper.assertTrue(HorrorSpeech.validAudio(packet.audio()), "El emisor y el consumidor no pueden alterar los bytes de un paquete ya validado");
            helper.assertTrue(!HorrorSpeech.validAudio(new byte[HorrorSpeech.MAX_AUDIO_BYTES + 1])
                && !HorrorSpeech.validAudio(new byte[]{'O', 'g', 'g', 'S'}), "No se acepta una firma vacía ni un audio que exceda el límite");
            try { new HorrorNetwork.Speech(UUID.randomUUID(), Double.NaN, 0, 0, packet.audio()); helper.fail("Se aceptaron coordenadas inválidas"); }
            catch (IllegalArgumentException expected) { }
            helper.succeed();
        } catch (IOException error) { throw new IllegalStateException(error); }
    }

    @GameTest(template = "empty", timeoutTicks = 20)
    public void voiceConfigStartsClosedAndRejectsUnknownFields(GameTestHelper helper) {
        Path root = null;
        try {
            root = Files.createTempDirectory("raps-voice-fixture-");
            var config = HorrorSpeech.load(root);
            helper.assertTrue(!config.enabled() && config.textFallback() && config.endpoint().isEmpty(),
                "No hay peticiones externas al instalar una configuración nueva");
            Path file = root.resolve("voice.json");
            Files.writeString(file, "{\"schemaVersion\":1,\"command\":\"shell\"}");
            try { HorrorSpeech.load(root); helper.fail("La configuración aceptó un campo de operación externo"); }
            catch (IOException expected) { }
            Files.writeString(file, "x".repeat(4097));
            try { HorrorSpeech.load(root); helper.fail("Se aceptó un archivo fuera de límite"); }
            catch (IOException expected) { }
            helper.succeed();
        } catch (IOException error) { throw new IllegalStateException(error); }
        finally {
            if (root != null) try { Files.deleteIfExists(root.resolve("voice.json")); Files.deleteIfExists(root); }
            catch (IOException ignored) { }
        }
    }
}