fix(optimiseur): un token recopié par l'optimiseur ne sort par aucun journal (brief 4 T7, correctif A)
Constat de T6 (question 2) : la trace masquait un token recopié dans reason ou planId, mais les lignes [Optimiseur] (planId, texte, détail des refus, nom du handshake) et GetOptimizerStatus.name le portaient en clair. - OptimizerSecret : UNE fonction de masquage (texte brut et échappé JSON, objet JSON à toute profondeur, clés comprises ; ligne de trace sérialisée), partagée par le client et les traces. - OptimizerClient masque chaque message reçu dès son analyse, avec le token envoyé sur cette connexion : rien en aval (journal, traces, app) ne voit le secret ; dit une fois. - Test testAnEchoedTokenLeaksNowhere : secret avec " et \ recopié partout ; aucune ligne de journal (tous niveaux), aucune trace, aucune réponse RPC. 4 contre-épreuves mordent. decisions sans endpoint : IDENTIQUE (23 792 lignes), 0 rejeu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
10683744d2
commit
693e01579f
@ -19,6 +19,7 @@ HEADERS += \
|
||||
$$PWD/scheduler/rulebasedscheduler.h \
|
||||
$$PWD/scheduler/optimizerprotocol.h \
|
||||
$$PWD/scheduler/optimizerclient.h \
|
||||
$$PWD/scheduler/optimizersecret.h \
|
||||
$$PWD/scheduler/optimizerplan.h \
|
||||
$$PWD/scheduler/socketscheduler.h \
|
||||
$$PWD/scheduler/optimizerstatus.h \
|
||||
@ -43,6 +44,7 @@ SOURCES += \
|
||||
$$PWD/scheduler/rulebasedscheduler.cpp \
|
||||
$$PWD/scheduler/optimizerprotocol.cpp \
|
||||
$$PWD/scheduler/optimizerclient.cpp \
|
||||
$$PWD/scheduler/optimizersecret.cpp \
|
||||
$$PWD/scheduler/optimizerplan.cpp \
|
||||
$$PWD/scheduler/socketscheduler.cpp \
|
||||
$$PWD/scheduler/optimizerstatus.cpp \
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||||
#include "optimizerclient.h"
|
||||
#include "optimizersecret.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
@ -302,6 +303,7 @@ void OptimizerClient::onConnected()
|
||||
// Relu à CHAQUE connexion (décision Q2) : un token remplacé vaut dès la tentative suivante.
|
||||
const QByteArray token = readToken();
|
||||
m_tokenSent = !token.isEmpty();
|
||||
m_tokenConnexion = QString::fromUtf8(token);
|
||||
if (m_tokenSent)
|
||||
params.insert(QStringLiteral("token"), QString::fromUtf8(token));
|
||||
|
||||
@ -392,7 +394,16 @@ void OptimizerClient::handleLine(const QByteArray &line)
|
||||
countProtocolError(QStringLiteral("invalid-json"), QStringLiteral("%1 octets").arg(line.size()));
|
||||
return;
|
||||
}
|
||||
const QJsonObject message = doc.object();
|
||||
// Correctif A (brief 4 T7) : un optimiseur peut RECOPIER le token qu'il a reçu. Masqué ICI,
|
||||
// avant toute lecture : nom, version, méthode, plan, motifs — rien en aval ne voit le secret,
|
||||
// ni le journal, ni les traces, ni l'app.
|
||||
int echos = 0;
|
||||
const QJsonObject message = OptimizerSecret::masquerJson(doc.object(), m_tokenConnexion, &echos);
|
||||
if (echos > 0 && !m_echoTokenSignale) {
|
||||
m_echoTokenSignale = true;
|
||||
qCWarning(dcNymeaEnergy()) << "[Optimiseur] l'optimiseur recopie le token dans ses réponses — masqué"
|
||||
" à la réception (journal, traces, app) ; signalé une fois";
|
||||
}
|
||||
const QJsonValue idValue = message.value(QStringLiteral("id"));
|
||||
|
||||
if (idValue.isUndefined() || idValue.isNull()) {
|
||||
@ -553,6 +564,7 @@ void OptimizerClient::dropSocket()
|
||||
tcp->abort();
|
||||
socket->deleteLater();
|
||||
m_buffer.clear();
|
||||
m_tokenConnexion.clear();
|
||||
m_skippingLine = false;
|
||||
}
|
||||
|
||||
|
||||
@ -118,6 +118,11 @@ private:
|
||||
* **Le token** est relu dans \c tokenPath à CHAQUE tentative de connexion : le remplacer prend
|
||||
* effet à la suivante, sans redémarrage. Il ne figure dans aucune ligne de journal, ni en clair
|
||||
* ni par sa longueur ; le message de handshake qui le porte n'est jamais journalisé.
|
||||
* **Un optimiseur qui le recopie** (\c reason, \c planId, \c name…) ne le fait pas sortir : chaque
|
||||
* message reçu est masqué dès son analyse (\c OptimizerSecret::masquerJson(), avec le token
|
||||
* envoyé sur CETTE connexion), avant toute journalisation et avant tout signal — journal, traces
|
||||
* et charges utiles de l'app ne voient que \c "[token masqué]". Le fait est dit une fois par
|
||||
* client, jamais la valeur.
|
||||
*
|
||||
* \invariant Aucune méthode n'attend : toute E/S est asynchrone. \c requestPlan() rend la main
|
||||
* immédiatement ; la réponse arrive par signal, au plus tôt au tour de boucle suivant.
|
||||
@ -291,6 +296,8 @@ private:
|
||||
quint64 m_handshakeId = 0;
|
||||
quint64 m_pendingId = 0;
|
||||
bool m_tokenSent = false; // le handshake en cours porte un token (pour le diagnostic)
|
||||
QString m_tokenConnexion; // le token envoyé sur CETTE connexion : ce qu'un écho recopierait
|
||||
bool m_echoTokenSignale = false;
|
||||
|
||||
qint64 m_handshakeDeadline = -1; // -1 = pas d'échéance
|
||||
qint64 m_requestDeadline = -1;
|
||||
|
||||
83
energyplugin/etm/scheduler/optimizersecret.cpp
Normal file
83
energyplugin/etm/scheduler/optimizersecret.cpp
Normal file
@ -0,0 +1,83 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||||
|
||||
#include "optimizersecret.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
|
||||
namespace OptimizerSecret {
|
||||
|
||||
namespace {
|
||||
|
||||
// La forme que QJsonDocument donne au token DANS une chaîne : ["…"] sans ses bornes.
|
||||
QByteArray formeEchappee(const QString &token)
|
||||
{
|
||||
const QByteArray tableau = QJsonDocument(QJsonArray{token}).toJson(QJsonDocument::Compact);
|
||||
return tableau.mid(2, tableau.size() - 4);
|
||||
}
|
||||
|
||||
QJsonValue masquerValeur(const QJsonValue &v, const QString &token, int *masques)
|
||||
{
|
||||
switch (v.type()) {
|
||||
case QJsonValue::String:
|
||||
return masquerTexte(v.toString(), token, masques);
|
||||
case QJsonValue::Array: {
|
||||
QJsonArray out;
|
||||
for (const QJsonValue &x : v.toArray())
|
||||
out.append(masquerValeur(x, token, masques));
|
||||
return out;
|
||||
}
|
||||
case QJsonValue::Object:
|
||||
return masquerJson(v.toObject(), token, masques);
|
||||
default:
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QString masquerTexte(const QString &texte, const QString &token, int *masques)
|
||||
{
|
||||
if (token.isEmpty())
|
||||
return texte;
|
||||
QString out = texte;
|
||||
// L'échappée d'abord : elle CONTIENT la brute quand le token n'a ni " ni \ (elles sont alors
|
||||
// égales), et sinon un optimiseur qui a recopié la forme sérialisée l'a fait sous celle-là.
|
||||
for (const QString &forme : {QString::fromUtf8(formeEchappee(token)), token}) {
|
||||
const int n = out.count(forme);
|
||||
if (n == 0)
|
||||
continue;
|
||||
out.replace(forme, QString::fromUtf8(Masque)); // UTF-8 : « masqué »
|
||||
if (masques)
|
||||
*masques += n;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QJsonObject masquerJson(const QJsonObject &message, const QString &token, int *masques)
|
||||
{
|
||||
if (token.isEmpty())
|
||||
return message;
|
||||
QJsonObject out;
|
||||
for (auto it = message.constBegin(); it != message.constEnd(); ++it)
|
||||
out.insert(masquerTexte(it.key(), token, masques), masquerValeur(it.value(), token, masques));
|
||||
return out;
|
||||
}
|
||||
|
||||
QByteArray masquerLigneJson(const QByteArray &ligne, const QByteArray &token, int *masques)
|
||||
{
|
||||
if (token.isEmpty())
|
||||
return ligne;
|
||||
const QByteArray echappe = formeEchappee(QString::fromUtf8(token));
|
||||
if (echappe.isEmpty() || !ligne.contains(echappe))
|
||||
return ligne;
|
||||
QByteArray out = ligne;
|
||||
const int n = out.count(echappe);
|
||||
out.replace(echappe, Masque);
|
||||
if (masques)
|
||||
*masques += n;
|
||||
return out;
|
||||
}
|
||||
|
||||
}
|
||||
56
energyplugin/etm/scheduler/optimizersecret.h
Normal file
56
energyplugin/etm/scheduler/optimizersecret.h
Normal file
@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
/*!
|
||||
* \file optimizersecret.h
|
||||
* \brief Masquage du token d'optimiseur (brief 4 T7, correctif A) — LA fonction partagée par le
|
||||
* journal, les traces et tout ce qui part vers l'app.
|
||||
*
|
||||
* Le token ne voyage que dans le handshake (§3). Un optimiseur qui le RECOPIE dans ce qu'il
|
||||
* renvoie (\c reason, \c planId, un code, un paramètre, son \c name…) ferait sortir le secret par
|
||||
* chacun des chemins qui relaient son texte : journal \c [Optimiseur], traces, \c GetOptimizerStatus.
|
||||
* Plutôt que de masquer à chacun de ces chemins — et d'oublier le prochain —, le client masque
|
||||
* chaque message REÇU dès son analyse (\c OptimizerClient), avant tout usage : rien en aval ne voit
|
||||
* jamais le secret. L'écrivain des traces masque en plus la ligne sérialisée (défense en
|
||||
* profondeur), avec la même fonction.
|
||||
*
|
||||
* \invariant Aucune fonction d'ici ne journalise, ni le token, ni sa longueur.
|
||||
*/
|
||||
namespace OptimizerSecret {
|
||||
|
||||
//! Ce qui remplace le token, partout.
|
||||
constexpr const char *Masque = "[token masqué]";
|
||||
|
||||
/*!
|
||||
* \brief Remplace dans un TEXTE chaque occurrence du token, sous sa forme brute et sous sa forme
|
||||
* échappée JSON (\c " et \c \\ précédés d'un \c \\).
|
||||
* \param texte Texte à masquer. \param token Le secret ; vide = rien à faire.
|
||||
* \param[in,out] masques Incrémenté du nombre de remplacements (peut être \c nullptr).
|
||||
* \return Le texte masqué.
|
||||
*/
|
||||
QString masquerTexte(const QString &texte, const QString &token, int *masques = nullptr);
|
||||
|
||||
/*!
|
||||
* \brief Masque un message JSON reçu : chaque chaîne ET chaque clé d'objet, à toute profondeur.
|
||||
* \param message Objet analysé. \param token Le secret ; vide = rien à faire.
|
||||
* \param[in,out] masques Incrémenté du nombre de remplacements (peut être \c nullptr).
|
||||
* \return L'objet masqué. Les nombres, booléens et la structure sont inchangés.
|
||||
*/
|
||||
QJsonObject masquerJson(const QJsonObject &message, const QString &token, int *masques = nullptr);
|
||||
|
||||
/*!
|
||||
* \brief Masque une ligne JSON DÉJÀ SÉRIALISÉE (\c QJsonDocument, compacte) : le token y est cherché
|
||||
* sous sa seule forme échappée — c'est ainsi, et seulement ainsi, qu'il y figurerait. Chercher la
|
||||
* forme brute pourrait mordre sur la syntaxe de la ligne et la rendre illisible.
|
||||
* \param ligne Ligne JSON. \param token Le secret, brut ; vide = rien à faire.
|
||||
* \param[in,out] masques Incrémenté du nombre de remplacements (peut être \c nullptr).
|
||||
* \return La ligne masquée.
|
||||
*/
|
||||
QByteArray masquerLigneJson(const QByteArray &ligne, const QByteArray &token, int *masques = nullptr);
|
||||
|
||||
}
|
||||
@ -14,6 +14,7 @@
|
||||
#include <nymeasettings.h>
|
||||
|
||||
#include "energysettings.h"
|
||||
#include "optimizersecret.h"
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
||||
|
||||
@ -122,25 +123,6 @@ QVariantList executees(const Slot &slot, const QHash<QString, LoadAction> &appli
|
||||
return l;
|
||||
}
|
||||
|
||||
QByteArray masquerToken(const QByteArray &ligne, const QByteArray &token, int *masques)
|
||||
{
|
||||
if (masques)
|
||||
*masques = 0;
|
||||
if (token.isEmpty())
|
||||
return ligne;
|
||||
// La forme que QJsonDocument donnerait au token dans une chaîne : ["…"] sans ses bornes.
|
||||
const QByteArray tableau = QJsonDocument(QJsonArray{QString::fromUtf8(token)}).toJson(QJsonDocument::Compact);
|
||||
const QByteArray echappe = tableau.mid(2, tableau.size() - 4);
|
||||
if (echappe.isEmpty() || !ligne.contains(echappe))
|
||||
return ligne;
|
||||
QByteArray out = ligne;
|
||||
const int n = out.count(echappe);
|
||||
out.replace(echappe, QByteArrayLiteral("[token masqué]"));
|
||||
if (masques)
|
||||
*masques = n;
|
||||
return out;
|
||||
}
|
||||
|
||||
Reglage Reglage::lire()
|
||||
{
|
||||
EnergySettings s;
|
||||
@ -196,7 +178,9 @@ void OptimizerTraceWriter::ecrire(const QVariantMap &ligne, const QDate &jour)
|
||||
} decompte{m_enAttente.get()};
|
||||
|
||||
int masques = 0;
|
||||
const QByteArray l = OptimizerTrace::masquerToken(
|
||||
// Seconde garde : le client masque déjà chaque message reçu (OptimizerSecret, correctif A) ;
|
||||
// celle-ci couvre un token REMPLACÉ depuis la connexion, que l'écrivain relit du fichier.
|
||||
const QByteArray l = OptimizerSecret::masquerLigneJson(
|
||||
QJsonDocument(QJsonObject::fromVariantMap(ligne)).toJson(QJsonDocument::Compact),
|
||||
token(), &masques)
|
||||
+ '\n';
|
||||
|
||||
@ -32,8 +32,9 @@
|
||||
* \c t (instant du cycle, heure locale avec décalage), \c request (les paramètres \c GetPlan
|
||||
* envoyés, ou ceux qui l'auraient été), \c response (ce qui a répondu à CETTE requête),
|
||||
* \c executed (actions appliquées au cycle), \c strategy (\c optimizer | \c rules | \c none),
|
||||
* \c latencyMs (optimiseur seulement). Jamais le token : il ne voyage que dans le handshake,
|
||||
* et l'écrivain masque en plus toute occurrence que la réponse brute d'un optimiseur porterait.
|
||||
* \c latencyMs (optimiseur seulement). Jamais le token : il ne voyage que dans le handshake ;
|
||||
* le client masque toute réponse qui le recopierait (\c OptimizerSecret), et l'écrivain masque
|
||||
* encore la ligne sérialisée, avec la même fonction.
|
||||
*
|
||||
* \invariant Les traces restent sur le site (invariant 10 du moteur) : aucun envoi, export manuel.
|
||||
* \invariant Rien ici ne commande : une trace activée ou non, en échec ou non, ne change aucune
|
||||
@ -99,17 +100,6 @@ QVariantMap reponseRegles(const Plan &plan, const QDateTime &now);
|
||||
QVariantList executees(const Slot &slot, const QHash<QString, LoadAction> &appliquees,
|
||||
const QSet<QString> &delestees);
|
||||
|
||||
/*!
|
||||
* \brief Remplace, dans une ligne déjà sérialisée, chaque occurrence du token par
|
||||
* \c "[token masqué]".
|
||||
* \param ligne Ligne JSON. \param token Le secret, brut ; vide = rien à faire.
|
||||
* \param[out] masques Nombre de remplacements (peut être \c nullptr).
|
||||
* \return La ligne masquée.
|
||||
* \note Le token est cherché sous sa forme ÉCHAPPÉE JSON (\c " et \c \\ ) : c'est ainsi qu'il
|
||||
* apparaîtrait dans la ligne.
|
||||
*/
|
||||
QByteArray masquerToken(const QByteArray &ligne, const QByteArray &token, int *masques = nullptr);
|
||||
|
||||
/*! \brief Réglage des traces (\c Optimizer/trace* dans \c energy.conf). */
|
||||
struct Reglage {
|
||||
bool active = false; //!< \c Optimizer/trace — défaut : désactivé.
|
||||
|
||||
@ -14,8 +14,10 @@ TARGET = etm-powersync-optimizerclient
|
||||
INCLUDEPATH += $$PWD/../../../energyplugin
|
||||
|
||||
HEADERS += $$PWD/../../../energyplugin/etm/scheduler/optimizerclient.h \
|
||||
$$PWD/../../../energyplugin/etm/scheduler/optimizersecret.h \
|
||||
$$PWD/../common/etm-powersync-fakeoptimizer.h
|
||||
SOURCES += $$PWD/../../../energyplugin/etm/scheduler/optimizerclient.cpp \
|
||||
$$PWD/../../../energyplugin/etm/scheduler/optimizersecret.cpp \
|
||||
testoptimizerclient.cpp
|
||||
|
||||
target.path = $$[QT_INSTALL_PREFIX]/bin
|
||||
|
||||
@ -134,6 +134,8 @@ private slots:
|
||||
void testTokenIsInNoTrace();
|
||||
void testTraceLineSize();
|
||||
void testTracerPairsTheResponseByRequestId();
|
||||
// Brief 4 T7 — correctif A : un token recopié par l'optimiseur ne sort nulle part
|
||||
void testAnEchoedTokenLeaksNowhere();
|
||||
|
||||
private:
|
||||
//! Banc commun ; \p avecOptimiseur faux = rien n'est configuré.
|
||||
@ -1944,5 +1946,107 @@ void TestSocketScheduler::testTracerPairsTheResponseByRequestId()
|
||||
QCOMPARE(l.first().value("latencyMs").toInt(), 12);
|
||||
}
|
||||
|
||||
void TestSocketScheduler::testAnEchoedTokenLeaksNowhere()
|
||||
{
|
||||
// Correctif A (brief 4 T7). Un optimiseur qui RECOPIE le token — avec les deux caractères que
|
||||
// JSON échappe — dans tout ce qu'il renvoie et que le moteur relaie : son nom et sa version au
|
||||
// handshake, l'identifiant et la stratégie du plan, le texte d'une action, un code hors
|
||||
// catalogue, une clé et une valeur de paramètre de motif, le texte d'une abstention, un loadId
|
||||
// inconnu, une notification. Le secret ne doit apparaître dans AUCUNE ligne de journal (tous
|
||||
// niveaux, nymea compris, hors la trace brute des données ENTRANTES de nymea — voir
|
||||
// testTokenIsWriteOnlyAndNeverLeaks), aucune trace, aucune réponse ni notification RPC.
|
||||
monter();
|
||||
const QString secret = QStringLiteral("T7-echo\"du\\token-9876543210");
|
||||
const QByteArray echappe = QJsonDocument(QJsonArray{secret}).toJson(QJsonDocument::Compact).mid(2).chopped(2);
|
||||
const QStringList fragments{secret, QString::fromUtf8(echappe), QStringLiteral("9876543210")};
|
||||
QCOMPARE(rpc("SetOptimizerToken", {{"token", secret}}).value("energyError").toString(),
|
||||
QString("EnergyErrorNoError"));
|
||||
const QString d = activerTraces();
|
||||
QSignalSpy paquets(m_mockTcpServer, &MockTcpServer::outgoingData);
|
||||
QLoggingCategory::setFilterRules(QStringLiteral("*.debug=true"));
|
||||
Journal j;
|
||||
|
||||
const QJsonObject handshakeAvant = m_handshake;
|
||||
m_handshake = handshakeResult();
|
||||
m_handshake.insert("name", "écho " + secret);
|
||||
m_handshake.insert("protocolVersion", "1.1-" + secret);
|
||||
const int handshakes = m_handshakes.count();
|
||||
m_serveur->abortClient();
|
||||
QVERIFY(waitFor([this, handshakes] { return m_handshakes.count() > handshakes; }, 8000));
|
||||
QCOMPARE(m_handshakes.last().value("token").toString(), secret); // le secret a bien transité
|
||||
SocketScheduler *s = m_arb->socketScheduler();
|
||||
QVERIFY(waitFor([s] { return s->client()->state() == OptimizerClient::Ready; }));
|
||||
|
||||
m_repondre = [this, secret](const QVariantMap &req) {
|
||||
QVariantMap p = plan(req, {action("ecs", {{"powerW", 1000}, {"reason", "écho " + secret},
|
||||
{"reasonCode", "SURPLUS_SETPOINT"},
|
||||
{"reasonParams", QVariantMap{{"budgetW", secret}, {secret, 5},
|
||||
{"setpointW", 1000}}}}),
|
||||
action(m_var, {{"powerW", 500}, {"reasonCode", secret}})});
|
||||
p.insert("planId", secret);
|
||||
p.insert("strategy", secret);
|
||||
p.insert("diagnostics", QVariantMap{{"status", "optimal"}, {"solver", secret}});
|
||||
return p;
|
||||
};
|
||||
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
|
||||
cycle(t0, -3000);
|
||||
cycle(t0.addSecs(60), -3000);
|
||||
QVERIFY(s->optimiseurAuDernierCycle());
|
||||
QCOMPARE(rpc("GetOptimizerStatus").value("name").toString(), QString("écho [token masqué]"));
|
||||
rpc("GetPlan");
|
||||
m_arb->loadTelemetry();
|
||||
injectAndWait("NymeaEnergy.GetLoadTelemetry");
|
||||
|
||||
m_repondre = [secret](const QVariantMap &) { return QVariantMap{{"abstain", true}, {"reason", "écho " + secret}}; };
|
||||
cycle(t0.addSecs(120), -3000);
|
||||
m_repondre = [this, secret](const QVariantMap &req) {
|
||||
return plan(req, {action("ecs", {{"powerW", 1000}}), action(m_var, {{"powerW", 500}}),
|
||||
action(secret, {{"powerW", 1}})});
|
||||
};
|
||||
cycle(t0.addSecs(180), -3000);
|
||||
m_serveur->sendJson(QJsonObject{{"jsonrpc", "2.0"}, {"method", secret}, {"params", QJsonObject{{secret, secret}}}});
|
||||
settle(200);
|
||||
cycle(t0.addSecs(240), -3000);
|
||||
rpc("GetOptimizerStatus");
|
||||
rpc("GetPlan");
|
||||
|
||||
// Le texte de l'optimiseur a bien coulé au journal — MASQUÉ.
|
||||
QVERIFY2(j.texte().contains("plan [token masqué] reçu"), qUtf8Printable(j.texte().right(3000)));
|
||||
QVERIFY(j.texte().contains("« écho [token masqué] »"));
|
||||
QVERIFY(j.texte().contains("abstention : écho [token masqué]"));
|
||||
QVERIFY(j.texte().contains("charge [token masqué] inconnue"));
|
||||
QCOMPARE(j.compte("recopie le token"), 1);
|
||||
// Aucune ligne du journal hors la trace brute ENTRANTE de nymea (la pose du token).
|
||||
for (const QString &f : fragments) {
|
||||
QStringList fuites;
|
||||
for (const QString &x : j.lignes(f))
|
||||
if (!x.startsWith(QLatin1String("JsonRpcTraffic: ")))
|
||||
fuites << x.left(300);
|
||||
QVERIFY2(fuites.isEmpty(), qUtf8Printable(fuites.join('\n')));
|
||||
}
|
||||
// Aucune réponse ni notification RPC (5 réponses au moins : 2 statuts, 2 plans, la télémétrie).
|
||||
QVERIFY2(paquets.count() >= 5, qPrintable(QString::number(paquets.count())));
|
||||
for (const QList<QVariant> &p : paquets)
|
||||
for (const QString &f : fragments)
|
||||
QVERIFY2(!p.last().toByteArray().contains(f.toUtf8()), p.last().toByteArray().left(300).constData());
|
||||
// Aucune trace.
|
||||
QVERIFY(traces(d, 5).count() >= 5);
|
||||
QByteArray tout;
|
||||
for (const QString &nom : fichiersTrace(d)) {
|
||||
QFile f(QDir(d).filePath(nom));
|
||||
QVERIFY(f.open(QIODevice::ReadOnly));
|
||||
tout += f.readAll();
|
||||
}
|
||||
QVERIFY(tout.contains("[token masqué]"));
|
||||
for (const QString &f : fragments)
|
||||
QVERIFY2(!tout.contains(f.toUtf8()), qPrintable(f));
|
||||
|
||||
m_handshake = handshakeAvant;
|
||||
QFile::remove(OptimizerClient::defaultTokenPath());
|
||||
QLoggingCategory::setFilterRules(QStringLiteral(
|
||||
"*.debug=false\nApplication.debug=true\nSimulation.debug=true\nExperiences.debug=true\n"
|
||||
"NymeaEnergy.debug=true\nEnergyMocks.debug=true\nDBus.warning=false"));
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestSocketScheduler)
|
||||
#include "testsocketscheduler.moc"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user