SetLoadConfig : le journal du refus ne recopie plus la saisie du client (relecture q7-suite, I-1)
Le refus écrivait, en noquote(), la phrase détaillée du refus : elle cite mode, domaine, rankOrigin, adaptateur, relays[].thingId, échéance et libellé tels que saisis. Un saut de ligne dans l'un d'eux fabriquait une fausse ligne de journal (etm72 l'échappait par `<< why`). Le journal ne porte plus que la phrase rendue depuis le motif (renderFr), comme SetTariff : cause et endroit, un identifiant seulement s'il est bien formé. Échapper le détail aurait suffi contre l'injection, pas contre le volume : relays[].thingId n'est pas borné. La cause système d'un WRITE_FAILED, que seul le détail portait, est désormais écrite par LoadConfigStore::commit() dans une ligne à part (chemin et erreur du fichier, aucune saisie). Test : testSetLoadConfigRefusalIsPublished envoie dix refus dont un champ vaut "x\n[NymeaEnergy] FAUX" et vérifie, message par message (JournalCapture::messages()), que le refus est journalisé et qu'aucun message n'ajoute de ligne commençant par [NymeaEnergy]. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b793e07787
commit
2ce74892d3
@ -430,8 +430,14 @@ bool LoadConfigStore::commit(const LoadConfigs &configs, int premierBorne, QStri
|
|||||||
const LoadConfigs previous = m_configs;
|
const LoadConfigs previous = m_configs;
|
||||||
m_configs = configs;
|
m_configs = configs;
|
||||||
|
|
||||||
if (!save(error)) {
|
QString echec;
|
||||||
|
if (!save(&echec)) {
|
||||||
m_configs = previous; // rollback mémoire si l'écriture échoue
|
m_configs = previous; // rollback mémoire si l'écriture échoue
|
||||||
|
// La cause système est dite ICI : le refus de SetLoadConfig ne journalise que son motif,
|
||||||
|
// et WRITE_FAILED ne dit pas pourquoi le disque a refusé. Aucune saisie n'y entre.
|
||||||
|
qCWarning(dcNymeaEnergy()) << "[LoadConfigStore] Écriture de la configuration échouée :"
|
||||||
|
<< filePath() << echec;
|
||||||
|
if (error) *error = echec;
|
||||||
if (fault)
|
if (fault)
|
||||||
*fault = QVariantMap{{QStringLiteral("cause"), QStringLiteral("WRITE_FAILED")}};
|
*fault = QVariantMap{{QStringLiteral("cause"), QStringLiteral("WRITE_FAILED")}};
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -1150,45 +1150,44 @@ JsonReply *NymeaEnergyJsonHandler::GetLoadConfig(const QVariantMap ¶ms)
|
|||||||
JsonReply *NymeaEnergyJsonHandler::SetLoadConfig(const QVariantMap ¶ms)
|
JsonReply *NymeaEnergyJsonHandler::SetLoadConfig(const QVariantMap ¶ms)
|
||||||
{
|
{
|
||||||
// Même forme que SetTariff : le code d'erreur amont, inchangé, et à côté le MOTIF publiable —
|
// Même forme que SetTariff : le code d'erreur amont, inchangé, et à côté le MOTIF publiable —
|
||||||
// une cause et un endroit, jamais une phrase (règle 12). Le journal garde la phrase
|
// une cause et un endroit, jamais une phrase (règle 12). Le journal ne porte que la phrase
|
||||||
// française rendue depuis ce motif, suivie du détail du refus.
|
// française rendue depuis ce motif, comme pour SetTariff : la phrase détaillée du refus
|
||||||
auto refuser = [this](const QVariantMap &faultParams, const QString &detail) {
|
// recopie ce que le client a saisi (mode, domaine, relais, libellé…), et un saut de ligne y
|
||||||
|
// fabriquerait une fausse ligne de journal (relecture q7-suite, I-1).
|
||||||
|
auto refuser = [this](const QVariantMap &faultParams) {
|
||||||
// 7-d — le refus au moins aussi haut que l'enregistrement.
|
// 7-d — le refus au moins aussi haut que l'enregistrement.
|
||||||
qCWarning(dcNymeaEnergy()).noquote()
|
qCWarning(dcNymeaEnergy()).noquote()
|
||||||
<< QStringLiteral("[NymeaEnergy] SetLoadConfig rejetée : %1 (motif %2)%3")
|
<< QStringLiteral("[NymeaEnergy] SetLoadConfig rejetée : %1 (motif %2)")
|
||||||
.arg(renderFr({DecisionCode::LoadConfigRefused, faultParams}),
|
.arg(renderFr({DecisionCode::LoadConfigRefused, faultParams}),
|
||||||
QLatin1String(DecisionCode::LoadConfigRefused),
|
QLatin1String(DecisionCode::LoadConfigRefused));
|
||||||
detail.isEmpty() ? QString() : QStringLiteral(" — ") + detail);
|
|
||||||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)},
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)},
|
||||||
{"fault", QLatin1String(DecisionCode::LoadConfigRefused)},
|
{"fault", QLatin1String(DecisionCode::LoadConfigRefused)},
|
||||||
{"faultParams", faultParams}});
|
{"faultParams", faultParams}});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!m_loadConfigStore)
|
if (!m_loadConfigStore)
|
||||||
return refuser({{QStringLiteral("cause"), QStringLiteral("STORE_UNAVAILABLE")}},
|
return refuser({{QStringLiteral("cause"), QStringLiteral("STORE_UNAVAILABLE")}});
|
||||||
QStringLiteral("aucun store configuré"));
|
|
||||||
|
|
||||||
LoadConfigs configs;
|
LoadConfigs configs;
|
||||||
foreach (const QVariant &item, params.value("loadConfigs").toList())
|
foreach (const QVariant &item, params.value("loadConfigs").toList())
|
||||||
configs.append(LoadConfig::fromMap(item.toMap()));
|
configs.append(LoadConfig::fromMap(item.toMap()));
|
||||||
|
|
||||||
QString why;
|
|
||||||
QVariantMap faultParams;
|
QVariantMap faultParams;
|
||||||
// Q7 — les bornes de saisie AVANT tout le reste, réconciliation comprise : ses motifs citent
|
// Q7 — les bornes de saisie AVANT tout le reste, réconciliation comprise : le motif ne cite
|
||||||
// l'identifiant et le libellé, qui doivent donc être bornés avant d'entrer au journal.
|
// un identifiant que bien formé.
|
||||||
if (!LoadConfigStore::validateBounds(configs, &why, &faultParams))
|
if (!LoadConfigStore::validateBounds(configs, nullptr, &faultParams))
|
||||||
return refuser(faultParams, why);
|
return refuser(faultParams);
|
||||||
|
|
||||||
// LM-1209-c — la marque de rang se réconcilie ICI, à la frontière CLIENTE, et nulle part
|
// LM-1209-c — la marque de rang se réconcilie ICI, à la frontière CLIENTE, et nulle part
|
||||||
// ailleurs : c'est la seule couche qui sache qu'une écriture vient du dehors. Le moteur,
|
// ailleurs : c'est la seule couche qui sache qu'une écriture vient du dehors. Le moteur,
|
||||||
// lui, pose légitimement "auto" en créant une entrée d'office.
|
// lui, pose légitimement "auto" en créant une entrée d'office.
|
||||||
LoadConfigs reconciliees;
|
LoadConfigs reconciliees;
|
||||||
if (!m_loadConfigStore->reconcileRankOrigin(configs, &reconciliees, &why, &faultParams))
|
if (!m_loadConfigStore->reconcileRankOrigin(configs, &reconciliees, nullptr, &faultParams))
|
||||||
return refuser(faultParams, why);
|
return refuser(faultParams);
|
||||||
configs = reconciliees;
|
configs = reconciliees;
|
||||||
|
|
||||||
if (!m_loadConfigStore->setConfigs(configs, &why, &faultParams))
|
if (!m_loadConfigStore->setConfigs(configs, nullptr, &faultParams))
|
||||||
return refuser(faultParams, why);
|
return refuser(faultParams);
|
||||||
// store.setConfigs() a déjà persisté + émis changed() → l'arbitre reconstruit, la notif part.
|
// store.setConfigs() a déjà persisté + émis changed() → l'arbitre reconstruit, la notif part.
|
||||||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -147,6 +147,8 @@ public:
|
|||||||
bool contains(const QString &fragment) const { return m_texte.contains(fragment); }
|
bool contains(const QString &fragment) const { return m_texte.contains(fragment); }
|
||||||
bool contient(QtMsgType type) const { return m_types.contains(type); }
|
bool contient(QtMsgType type) const { return m_types.contains(type); }
|
||||||
QString texte() const { return m_texte; }
|
QString texte() const { return m_texte; }
|
||||||
|
//! Les messages un par un : une fausse ligne de journal ne se voit qu'À L'INTÉRIEUR d'un message.
|
||||||
|
QStringList messages() const { return m_messages; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void collecte(QtMsgType type, const QMessageLogContext &ctx, const QString &msg)
|
static void collecte(QtMsgType type, const QMessageLogContext &ctx, const QString &msg)
|
||||||
@ -154,12 +156,14 @@ private:
|
|||||||
// Toutes les captures vivantes reçoivent le message — c'est ce que le chaînage visait.
|
// Toutes les captures vivantes reçoivent le message — c'est ce que le chaînage visait.
|
||||||
for (JournalCapture *c : s_pile) {
|
for (JournalCapture *c : s_pile) {
|
||||||
c->m_texte += msg + QLatin1Char('\n');
|
c->m_texte += msg + QLatin1Char('\n');
|
||||||
|
c->m_messages.append(msg);
|
||||||
c->m_types.insert(type);
|
c->m_types.insert(type);
|
||||||
}
|
}
|
||||||
if (s_origine)
|
if (s_origine)
|
||||||
s_origine(type, ctx, msg); // le message reste visible dans la sortie de test
|
s_origine(type, ctx, msg); // le message reste visible dans la sortie de test
|
||||||
}
|
}
|
||||||
QString m_texte;
|
QString m_texte;
|
||||||
|
QStringList m_messages;
|
||||||
QSet<QtMsgType> m_types;
|
QSet<QtMsgType> m_types;
|
||||||
static QList<JournalCapture *> s_pile;
|
static QList<JournalCapture *> s_pile;
|
||||||
static QtMessageHandler s_origine;
|
static QtMessageHandler s_origine;
|
||||||
@ -6247,6 +6251,46 @@ void Simulation::testSetLoadConfigRefusalIsPublished()
|
|||||||
QVERIFY2(lire() == avantGet, c.quoi);
|
QVERIFY2(lire() == avantGet, c.quoi);
|
||||||
QVERIFY2(fichier() == avantDisque, c.quoi);
|
QVERIFY2(fichier() == avantDisque, c.quoi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [relecture q7-suite, I-1] Le journal d'un refus ne recopie RIEN de la saisie. La phrase
|
||||||
|
// détaillée y partait sans échappement : un saut de ligne glissé dans un champ fabriquait une
|
||||||
|
// fausse ligne de journal. Chaque champ qu'elle citait, refusé tour à tour.
|
||||||
|
const QString forge = QStringLiteral("x\n[NymeaEnergy] FAUX");
|
||||||
|
QVariantMap ballonF = ballon;
|
||||||
|
ballonF.insert("relays", QVariantList{QVariantMap{{"thingId", forge}, {"powerW", 1000}}});
|
||||||
|
QVariantMap ballonCF = ballonC;
|
||||||
|
ballonCF.insert("relays", QVariantList{QVariantMap{{"thingId", forge}, {"powerW", 500}}});
|
||||||
|
struct Forge { const char *quoi; QVariantList charges; const char *cause; };
|
||||||
|
const QList<Forge> forges{
|
||||||
|
{"mode", avec(0, {{"mode", forge}}), "MODE"},
|
||||||
|
{"domain", avec(0, {{"domain", forge}}), "DOMAIN"},
|
||||||
|
{"rankOrigin", avec(0, {{"rankOrigin", forge}}), "RANK_ORIGIN"},
|
||||||
|
{"adapter", avec(0, {{"adapter", forge}}), "ADAPTER"},
|
||||||
|
{"relays[].thingId", avec(0, {{"relays", QVariantList{QVariantMap{{"thingId", forge}, {"powerW", 1000}},
|
||||||
|
QVariantMap{{"thingId", forge}, {"powerW", 500}}}}}),
|
||||||
|
"FIELD_DUPLICATE"},
|
||||||
|
{"dailyDeadline", avec(0, {{"needs", QVariantMap{{"dailyDeadline", forge}}}}), "DEADLINE"},
|
||||||
|
{"label (Config … invalide)", avec(0, {{"label", forge}, {"mode", "variable"}}), "MODE"},
|
||||||
|
{"label (rang en double)", avec(1, {{"label", forge}, {"priority", 1}}), "RANK_DUPLICATE"},
|
||||||
|
{"label (auto fabriqué)", avec(1, {{"label", forge}, {"rankOrigin", "auto"}}), "RANK_ORIGIN_AUTO"},
|
||||||
|
{"relais partagé", QVariantList{ballonF, routeurPv, ballonCF}, "THING_SHARED"},
|
||||||
|
};
|
||||||
|
for (const Forge &f : forges) {
|
||||||
|
JournalCapture journal;
|
||||||
|
const QVariantMap p = poser(f.charges).value("params").toMap();
|
||||||
|
QVERIFY2(p.value("faultParams").toMap().value("cause").toString() == QLatin1String(f.cause),
|
||||||
|
qPrintable(QStringLiteral("%1 : %2").arg(f.quoi, QString::fromUtf8(json(p.value("faultParams"))))));
|
||||||
|
// Non vide : le refus EST journalisé…
|
||||||
|
QVERIFY2(journal.contains(QStringLiteral("SetLoadConfig rejetée")), f.quoi);
|
||||||
|
// …et aucun message n'y ajoute une ligne qui se fasse passer pour un message du moteur.
|
||||||
|
for (const QString &m : journal.messages()) {
|
||||||
|
const QStringList lignes = m.split(QLatin1Char('\n'));
|
||||||
|
for (int i = 1; i < lignes.count(); ++i)
|
||||||
|
QVERIFY2(!lignes.at(i).startsWith(QLatin1String("[NymeaEnergy]"), Qt::CaseInsensitive),
|
||||||
|
qPrintable(QStringLiteral("%1 : fausse ligne de journal « %2 »").arg(f.quoi, lignes.at(i))));
|
||||||
|
}
|
||||||
|
QVERIFY2(lire() == avantGet, f.quoi);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user