Files
Encelado/Encelado/tests/Encelado.Tests/CredentialStoreTests.cs
T
Alby96andClaude Fable 5.1 4c26fd3209 Passa ai Correlation Baskets su eToro e rimuove i motori precedenti (4.0.0)
Perché: l'utente ha chiesto un bot che operi cinque basket di coppie forex
correlate su eToro, autonomo, con ledger, feed gratuiti e apprendimento
costruito da zero, e ha deciso di eliminare tutto ciò che restava delle
gestioni precedenti (Binance, cTrader/proba, ricerca con SQLite, GBDT, RL,
TA-Lib) e di non avere approvazioni manuali sui singoli ordini.

Cosa cambia:
- nuovo Core dei basket (cross sintetici, decisore, cost gate, sizing,
  esecutore leg-risk, backtest con PSR/DSR/PBO, livelli 0-3 di apprendimento),
  adattatore eToro Public API, motore autonomo con equity stop, kill-switch,
  riconciliazione, ledger append-only, calendario e notizie con sentiment;
- modalità Paper / Demo / Live (Live con flag e frase CONFERMO LIVE);
- interfaccia rifatta: barra in alto con tre schede, dashboard con i soli
  numeri principali, fuso orario selezionabile, test di rendering in PNG;
- corretto il parser dei costi eToro (campo "value"): markup e overnight
  non venivano letti;
- strumento di ricerca ridotto a ticks / baskets / falsify con due scenari di
  costo; risultati in results/ e reports/: nessuna configurazione è
  profittevole al netto dei costi (docs/STRATEGY.md lo dice con i numeri);
- documentazione completa (STRATEGY, ML_AND_LEARNING, RUNBOOK, GLOSSARY,
  KNOWN_ISSUES, ADR-0004, ADR-0005) e catena di rilascio aggiornata.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:45:55 +02:00

140 lines
5.0 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Encelado.Bot.Configuration;
namespace Encelado.Tests;
/// <summary>
/// Every test redirects the store to a scratch directory via ENCELADO_HOME, so the
/// developer's real saved keys are never read, written or deleted.
/// </summary>
public sealed class EtoroKeyStoreTests : IDisposable
{
private readonly string _home;
private readonly string? _previousHome;
public EtoroKeyStoreTests()
{
_previousHome = Environment.GetEnvironmentVariable("ENCELADO_HOME");
_home = Path.Combine(Path.GetTempPath(), $"encelado-store-{Guid.NewGuid():N}");
Environment.SetEnvironmentVariable("ENCELADO_HOME", _home);
}
public void Dispose()
{
Environment.SetEnvironmentVariable("ENCELADO_HOME", _previousHome);
if (Directory.Exists(_home))
{
Directory.Delete(_home, recursive: true);
}
}
[Fact]
public void HonoursTheHomeOverride() =>
Assert.Equal(Path.Combine(_home, "etoro.dat"), EtoroKeyStore.FilePath);
[Fact]
public void KeysRoundTripPerEnvironment()
{
Assert.False(EtoroKeyStore.Exists);
Assert.Null(EtoroKeyStore.Load(demo: true));
EtoroKeyStore.Save(demo: true, new EtoroKeys("api-demo", "user-demo", DateTime.UtcNow));
EtoroKeyStore.Save(demo: false, new EtoroKeys("api-real", "user-real", DateTime.UtcNow));
Assert.True(EtoroKeyStore.Exists);
Assert.Equal("api-demo", EtoroKeyStore.Load(demo: true)!.ApiKey);
Assert.Equal("user-real", EtoroKeyStore.Load(demo: false)!.UserKey);
Assert.True(EtoroKeyStore.Clear(demo: true));
Assert.Null(EtoroKeyStore.Load(demo: true));
Assert.NotNull(EtoroKeyStore.Load(demo: false));
Assert.True(EtoroKeyStore.Clear(demo: false));
Assert.False(EtoroKeyStore.Exists);
Assert.False(EtoroKeyStore.Clear(demo: false));
}
[Fact]
public void KeysAreNotReadableAsPlainTextOnWindows()
{
EtoroKeyStore.Save(demo: true, new EtoroKeys("api-key-value", "user-key-value", DateTime.UtcNow));
string onDisk = File.ReadAllText(EtoroKeyStore.FilePath);
if (EtoroKeyStore.IsEncrypted)
{
Assert.DoesNotContain("user-key-value", onDisk, StringComparison.Ordinal);
}
else
{
Assert.Contains("user-key-value", onDisk, StringComparison.Ordinal);
}
}
[Fact]
public void ACorruptFileIsTreatedAsAbsentRatherThanThrowing()
{
Directory.CreateDirectory(_home);
File.WriteAllBytes(EtoroKeyStore.FilePath, [0x00, 0x01, 0x02, 0x03, 0x04]);
Assert.Null(EtoroKeyStore.Load(demo: true));
EtoroKeyStore.Save(demo: true, new EtoroKeys("new-api", "new-user", DateTime.UtcNow));
Assert.Equal("new-api", EtoroKeyStore.Load(demo: true)!.ApiKey);
}
[Fact]
public void ResolvePrefersTheEnvironmentThenTheStore()
{
BotConfig config = new();
Assert.False(EtoroKeyStore.Resolve(config, out string origin));
Assert.Contains("nessuna", origin, StringComparison.OrdinalIgnoreCase);
EtoroKeyStore.Save(demo: true, new EtoroKeys("api-stored", "user-stored", DateTime.UtcNow));
Assert.True(EtoroKeyStore.Resolve(config, out origin));
Assert.Equal("api-stored", config.Etoro.ApiKey);
Assert.Contains("salvate", origin, StringComparison.OrdinalIgnoreCase);
BotConfig fromEnv = new();
fromEnv.Etoro.ApiKey = "api-env";
fromEnv.Etoro.UserKey = "user-env";
Assert.True(EtoroKeyStore.Resolve(fromEnv, out origin));
Assert.Equal("api-env", fromEnv.Etoro.ApiKey);
Assert.Contains("ambiente", origin, StringComparison.OrdinalIgnoreCase);
}
[Theory]
[InlineData("PKABCDEFGH1234", "PKABCD********")]
[InlineData("PKAB", "****")]
[InlineData("ab", "**")]
[InlineData("", "(vuota)")]
[InlineData(null, "(vuota)")]
public void MaskKeepsOnlyThePrefix(string? input, string expected) =>
Assert.Equal(expected, EtoroKeyStore.Mask(input));
[Theory]
[InlineData(" PKKEY123 ", "PKKEY123")]
[InlineData("PKKEY123", "PKKEY123")]
[InlineData("PKKEY123", "PKKEY123")]
[InlineData("P\0K\0K\0E\0Y\0", "PKKEY")]
[InlineData("PKKEY123\r\n", "PKKEY123")]
public void CleanStripsInvisibleCharactersFromPastedKeys(string raw, string expected) =>
Assert.Equal(expected, EtoroKeyStore.Clean(raw));
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("\r\n")]
public void CleanReturnsNullWhenNothingUsableRemains(string? raw) =>
Assert.Null(EtoroKeyStore.Clean(raw));
[Fact]
public void MaskNeverEchoesAWholeSecret()
{
const string secret = "abcdefghijklmnopqrstuvwxyz0123456789";
string masked = EtoroKeyStore.Mask(secret);
Assert.DoesNotContain(secret, masked, StringComparison.Ordinal);
Assert.DoesNotContain(secret[4..], masked, StringComparison.Ordinal);
Assert.StartsWith("abcd", masked, StringComparison.Ordinal);
}
}