feat/dataset-detail-chart #5
|
|
@ -35,7 +35,7 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void refreshCaptcha(); // 拉新验证码并重绘图片
|
void refreshCaptcha(); // 拉新验证码并重绘图片
|
||||||
void attemptLogin(); // 校验输入并发起阻塞登录
|
void attemptLogin(); // 校验输入并发起异步登录
|
||||||
void showError(const QString& msg);
|
void showError(const QString& msg);
|
||||||
|
|
||||||
geopro::net::AuthService& auth_;
|
geopro::net::AuthService& auth_;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ ApiChain::ApiChain(QList<StepFactory> steps, Predicate isFailure, QObject* paren
|
||||||
: QObject(parent), steps_(std::move(steps)), isFailure_(std::move(isFailure)) {
|
: QObject(parent), steps_(std::move(steps)), isFailure_(std::move(isFailure)) {
|
||||||
Q_ASSERT(!steps_.isEmpty()); // 契约:至少一步(空链永不发 succeeded)
|
Q_ASSERT(!steps_.isEmpty()); // 契约:至少一步(空链永不发 succeeded)
|
||||||
Q_ASSERT(isFailure_);
|
Q_ASSERT(isFailure_);
|
||||||
|
// 首步同步契约:此处同步执行首个 step 工厂。调用方须在 new ApiChain 之前连接
|
||||||
|
// succeeded/failed,且首个工厂不得同步抛出、其 IApiCall 不得同步 emit finished。
|
||||||
|
// 详见 ApiChain.hpp 首步同步契约说明。
|
||||||
runNext();
|
runNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,18 @@ namespace geopro::net {
|
||||||
// 与 ApiBatch 对称:同 Predicate、同信号面、同安全约束。
|
// 与 ApiBatch 对称:同 Predicate、同信号面、同安全约束。
|
||||||
// 注意:Part A(导航)暂未接入生产调用,首个生产使用见 Part B 登录(AuthService 串行链);
|
// 注意:Part A(导航)暂未接入生产调用,首个生产使用见 Part B 登录(AuthService 串行链);
|
||||||
// 已由 tests/net/test_api_chain.cpp 覆盖,请勿当死代码删除。
|
// 已由 tests/net/test_api_chain.cpp 覆盖,请勿当死代码删除。
|
||||||
|
//
|
||||||
|
// ── 首步同步契约(调用方必读)──────────────────────────────────────────────────
|
||||||
|
// ctor 内同步调用 runNext(),立即执行首个 step 工厂。
|
||||||
|
// 因此调用方须在 new ApiChain(...) 之前连接 succeeded/failed 信号——否则若首步工厂
|
||||||
|
// 同步抛异常(catch 后 emit failed),信号会在连接建立前发出而丢失。
|
||||||
|
// 契约:
|
||||||
|
// 1. 首个 step 工厂不得同步抛异常(生产路径:verifyCodeCheck 不抛,满足)。
|
||||||
|
// 2. 首个 step 工厂返回的 IApiCall 不得同步 emit finished(生产路径:真实网络请求,满足)。
|
||||||
|
// 若将来需要可同步完成的首步,应将 ctor 内 runNext() 改为
|
||||||
|
// QMetaObject::invokeMethod(this, &ApiChain::runNext, Qt::QueuedConnection)
|
||||||
|
// 以推迟首拍到事件循环,届时调用方已完成信号连接。
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────
|
||||||
class ApiChain : public QObject {
|
class ApiChain : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,10 @@ QString reasonOf(const ApiResponse& resp, const QString& fallback) {
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
CaptchaLoad::CaptchaLoad(IApiCall* call, QObject* parent) : QObject(parent), call_(call) {
|
CaptchaLoad::CaptchaLoad(IApiCall* call, QObject* parent) : QObject(parent), call_(call) {
|
||||||
QObject::connect(call, &IApiCall::finished, this, [this](const ApiResponse& resp) {
|
QObject::connect(call_, &IApiCall::finished, this, [this](const ApiResponse& resp) {
|
||||||
if (aborted_) return; // §5.0 入口守卫
|
if (aborted_) return; // §5.0 入口守卫
|
||||||
if (resp.code != 200 || !resp.rawError.isEmpty()) {
|
if (resp.code != 200 || !resp.rawError.isEmpty()) {
|
||||||
|
aborted_ = true; // 终态置位:到达 failed 终态后 abort() 早退
|
||||||
emit failed(reasonOf(resp, QStringLiteral("获取验证码失败")));
|
emit failed(reasonOf(resp, QStringLiteral("获取验证码失败")));
|
||||||
deleteLater();
|
deleteLater();
|
||||||
return;
|
return;
|
||||||
|
|
@ -28,6 +29,7 @@ CaptchaLoad::CaptchaLoad(IApiCall* call, QObject* parent) : QObject(parent), cal
|
||||||
AuthService::Captcha cap;
|
AuthService::Captcha cap;
|
||||||
cap.codeId = resp.data.value(QStringLiteral("id")).toString();
|
cap.codeId = resp.data.value(QStringLiteral("id")).toString();
|
||||||
cap.code = resp.data.value(QStringLiteral("code")).toString();
|
cap.code = resp.data.value(QStringLiteral("code")).toString();
|
||||||
|
aborted_ = true; // 终态置位:到达 done 终态后 abort() 早退
|
||||||
emit done(cap);
|
emit done(cap);
|
||||||
deleteLater();
|
deleteLater();
|
||||||
});
|
});
|
||||||
|
|
@ -41,7 +43,7 @@ void CaptchaLoad::abort() {
|
||||||
}
|
}
|
||||||
|
|
||||||
LoginLoad::LoginLoad(ApiChain* chain, QObject* parent) : QObject(parent), chain_(chain) {
|
LoginLoad::LoginLoad(ApiChain* chain, QObject* parent) : QObject(parent), chain_(chain) {
|
||||||
QObject::connect(chain, &ApiChain::succeeded, this,
|
QObject::connect(chain_, &ApiChain::succeeded, this,
|
||||||
[this](const QList<ApiResponse>& responses) {
|
[this](const QList<ApiResponse>& responses) {
|
||||||
if (aborted_) return; // §5.0 入口守卫
|
if (aborted_) return; // §5.0 入口守卫
|
||||||
const QString token =
|
const QString token =
|
||||||
|
|
@ -49,15 +51,18 @@ LoginLoad::LoginLoad(ApiChain* chain, QObject* parent) : QObject(parent), chain_
|
||||||
? QString()
|
? QString()
|
||||||
: responses.last().data.value(QStringLiteral("accessToken")).toString();
|
: responses.last().data.value(QStringLiteral("accessToken")).toString();
|
||||||
if (token.isEmpty()) {
|
if (token.isEmpty()) {
|
||||||
|
aborted_ = true; // 终态置位:到达 failed 终态后 abort() 早退
|
||||||
emit failed(QStringLiteral("登录成功但缺少 accessToken"));
|
emit failed(QStringLiteral("登录成功但缺少 accessToken"));
|
||||||
deleteLater();
|
deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
aborted_ = true; // 终态置位:到达 done 终态后 abort() 早退
|
||||||
emit done(token);
|
emit done(token);
|
||||||
deleteLater();
|
deleteLater();
|
||||||
});
|
});
|
||||||
QObject::connect(chain, &ApiChain::failed, this, [this](int, const ApiResponse& resp) {
|
QObject::connect(chain_, &ApiChain::failed, this, [this](int, const ApiResponse& resp) {
|
||||||
if (aborted_) return; // §5.0 入口守卫
|
if (aborted_) return; // §5.0 入口守卫
|
||||||
|
aborted_ = true; // 终态置位:到达 failed 终态后 abort() 早退
|
||||||
emit failed(reasonOf(resp, QStringLiteral("登录失败")));
|
emit failed(reasonOf(resp, QStringLiteral("登录失败")));
|
||||||
deleteLater();
|
deleteLater();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
// 用 FakeApiCall + 真 ApiChain 离线驱动,不联网。QSignalSpy 需 Qt6::Test。
|
// 用 FakeApiCall + 真 ApiChain 离线驱动,不联网。QSignalSpy 需 Qt6::Test。
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
#include <QSignalSpy>
|
#include <QSignalSpy>
|
||||||
|
|
||||||
#include "ApiChain.hpp"
|
#include "ApiChain.hpp"
|
||||||
|
|
@ -127,3 +129,25 @@ TEST(LoginLoad, EmitsFailedWhenTokenMissing) {
|
||||||
EXPECT_EQ(doneSpy.count(), 0);
|
EXPECT_EQ(doneSpy.count(), 0);
|
||||||
ASSERT_EQ(failSpy.count(), 1);
|
ASSERT_EQ(failSpy.count(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// M-2:step 工厂抛异常(模拟 RSA 失败)时 LoginLoad 应 emit failed。
|
||||||
|
// 第一步用 FakeApiCall(不同步 fire,由测试手动触发),第二步工厂抛异常。
|
||||||
|
// 构造顺序:new ApiChain(同步执行 step1 工厂,s1 已建立但未 fire)
|
||||||
|
// → new LoginLoad(连接 chain 的 succeeded/failed)
|
||||||
|
// → s1->fire(plainOk())(step1 成功 → step2 工厂抛 → ApiChain emit failed
|
||||||
|
// → LoginLoad 已连接,收到 failed)。
|
||||||
|
TEST(LoginLoad, EmitsFailedWhenStepFactoryThrows) {
|
||||||
|
auto* s1 = new FakeApiCall;
|
||||||
|
QList<ApiChain::StepFactory> steps{
|
||||||
|
[&](const QList<ApiResponse>&) -> IApiCall* { return s1; },
|
||||||
|
[&](const QList<ApiResponse>&) -> IApiCall* {
|
||||||
|
throw std::runtime_error("rsa fail");
|
||||||
|
}};
|
||||||
|
auto* chain = new ApiChain(steps, isFailure);
|
||||||
|
auto* load = new LoginLoad(chain);
|
||||||
|
QSignalSpy doneSpy(load, &LoginLoad::done);
|
||||||
|
QSignalSpy failSpy(load, &LoginLoad::failed);
|
||||||
|
s1->fire(plainOk()); // step1 成功 → step2 工厂抛异常 → ApiChain emit failed
|
||||||
|
ASSERT_EQ(failSpy.count(), 1);
|
||||||
|
EXPECT_EQ(doneSpy.count(), 0);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue