/* bench/account_cookie_sim.c — MUSIC_U 临期续期决策逻辑验证(§NEW,0 真实 NCM 请求)。 * * 用**真实** app/logic/account.c,仅把外围依赖换成桩(g_st 状态 + meow_config/log + * ncm_login_refresh(强制结果)+ ncm_ctx_set_cookie(记录落盘)+ ncm_store/json),验证 * account_cookie_maybe_refresh 的语义(用户定案): * ① 临期触发:cookie 年龄超阈值(12h)→ 后台静默刷一次(refresh 被调、新 cookie 应用、落盘 * last_refresh_ts 更新); * ② 未临期:年龄 ≤ 阈值 → 不刷(refresh 零调用); * ③ 首次/旧会话(last_refresh_ts=0,未知年龄)→ 视为临期刷一次; * ④ 失败退避:网络/TLS 瞬断 → 负错误码返回 + 冷却记录(30 分钟内重拨不重刷); * ⑤ 失效兜底:code301/-460 → NCM_ERR_AUTH + 置「需回登录页」标志(UI 退扫码); * ⑥ 冷却:上次失败 < 30 分钟(跨 boot 持久化 fail_ts)→ 即使临期也不刷。 * 风控纪律:0 次真实 NCM 请求;ncm_login_refresh 为桩、时间由 g_now 推动。 * 编译(宿主),在 ncm_app 目录:见 build_ncm_sims.sh 新增 account_cookie_sim 段。 * 运行:./account_cookie_sim */ #include "ncm.h" #include "ncm_app_internal.h" /* ncm_app_state_t / g_st / account_* 声明 */ #include #include #include /* ---- 真实 account.o 依赖的外围桩 ---- */ ncm_app_state_t g_st; /* 全局状态(app.c 通常定义;本桩自持) */ static int64_t g_now_ms = 0; /* 时钟(ncm_now_ms 桩返回,驱动年龄/冷却) */ int64_t ncm_now_ms(void) { return g_now_ms; } static int g_refresh_calls = 0; /* ncm_login_refresh 桩调用计数 */ static int g_refresh_rc = NCM_OK; /* 桩强制返回码 */ static const char *g_refresh_out = NULL; /* 成功时写入 out_cookie 的串 */ int ncm_login_refresh(const char *device_id, const char *cookie, char *out_cookie, size_t out_cookie_sz) { (void)device_id; (void)cookie; g_refresh_calls++; if (g_refresh_rc == NCM_OK && g_refresh_out) snprintf(out_cookie, out_cookie_sz, "%s", g_refresh_out); return g_refresh_rc; } static char g_applied_cookie[NCM_COOKIE_MAX]; /* ncm_ctx_set_cookie 落盘内容 */ static int g_set_cookie_calls = 0; int ncm_ctx_set_cookie(ncm_ctx_t *ctx, const char *cookie) { if (cookie) snprintf(ctx->cookie, sizeof ctx->cookie, "%s", cookie); else ctx->cookie[0] = 0; snprintf(g_applied_cookie, sizeof g_applied_cookie, "%s", cookie ? cookie : ""); g_set_cookie_calls++; return NCM_OK; } static char g_account_json[1024]; /* account_persist 写出的 account.json */ static int g_write_calls = 0; int ncm_store_read_file(const char *path, char *buf, size_t sz) { (void)path; (void)buf; (void)sz; return -1; } int ncm_store_write_file(const char *path, const void *data, size_t len) { (void)path; size_t n = len < sizeof g_account_json - 1 ? len : sizeof g_account_json - 1; memcpy(g_account_json, data, n); g_account_json[n] = 0; g_write_calls++; return NCM_OK; } /* account.o 引用的 json 便捷函数(本桩不调用 account_load,占位保证链接) */ int ncm_json_parse(ncm_json_t *j, const char *text, size_t len, jsmntok_t *toks, int tokcap, int *parent) { (void)j; (void)text; (void)len; (void)toks; (void)tokcap; (void)parent; return -1; } int ncm_json_path(const ncm_json_t *j, const char *path) { (void)j; (void)path; return -1; } int ncm_json_path_int(const ncm_json_t *j, const char *path, long *out) { (void)j; (void)path; if (out) *out = 0; return NCM_OK; } int64_t ncm_json_get_i64_at(const ncm_json_t *j, int tok, int *rc) { (void)j; (void)tok; if (rc) *rc = NCM_OK; return 0; } int ncm_api_vip_info(const char *d, const char *ck, const char *uid, ncm_vip_info_t *out) { (void)d; (void)ck; (void)uid; if (out) memset(out, 0, sizeof *out); return NCM_ERR_NOTFOUND; } static int g_cfg_hours = 12; /* ncm.cookie_refresh_h 配置 */ static int g_cfg_fail_ts = 0; /* ncm.cookie_refresh_fail_ts 配置(跨 boot 冷却) */ int meow_config_int(const char *key, int def) { if (strcmp(key, "ncm.cookie_refresh_h") == 0) return g_cfg_hours; if (strcmp(key, "ncm.cookie_refresh_fail_ts") == 0) return g_cfg_fail_ts; return def; } int meow_config_get(const char *key, char *out, int outsz) { (void)outsz; char b[24]; if (strcmp(key, "ncm.cookie_refresh_fail_ts") == 0) { snprintf(b, sizeof b, "%d", g_cfg_fail_ts); } else { snprintf(b, sizeof b, "%d", meow_config_int(key, 0)); } snprintf(out, (size_t)outsz, "%s", b); return 0; } int meow_config_set(const char *key, const char *val) { if (strcmp(key, "ncm.cookie_refresh_fail_ts") == 0) g_cfg_fail_ts = atoi(val); return 0; } void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; } /* ---- harness ---- */ static int g_fail = 0; #define CHECK(cond, fmt, ...) do { \ if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \ else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \ } while (0) #define NOW_S (1'700'000'000ll) /* 基准秒 */ #define COOKIE "MUSIC_U=OLD_TOKEN; __csrf=cs; MUSIC_A=anon" static void reset_state(int64_t last_refresh_ts) { memset(&g_st, 0, sizeof g_st); g_st.logged_in = 1; snprintf(g_st.ctx.cookie, sizeof g_st.ctx.cookie, "%s", COOKIE); snprintf(g_st.ctx.device_id, sizeof g_st.ctx.device_id, "AA11BB22CC33DD44EE55FF6600112233445566778899AABB00"); g_st.vip = NCM_VIP_NONE; g_now_ms = NOW_S * 1000; g_refresh_calls = 0; g_refresh_rc = NCM_OK; g_refresh_out = "MUSIC_U=NEW_TOKEN; __csrf=cs2; MUSIC_A=anon"; g_set_cookie_calls = 0; g_applied_cookie[0] = 0; g_account_json[0] = 0; g_write_calls = 0; g_cfg_hours = 12; g_cfg_fail_ts = 0; /* mark_fresh 重置 account.c 内单飞/冷却/回登录标志(跨用例需手动清) */ account_cookie_mark_fresh(); g_st.cookie_last_refresh_ts = last_refresh_ts; /* 覆盖为用例指定年龄 */ g_st.logged_in = 1; /* mark_fresh 不碰登录态,显式恢复 */ } int main(void) { puts("== account_cookie_sim: 临期续期决策 =="); /* ① 临期触发:年龄超阈值(12h)→ 后台静默刷一次,新 cookie 应用 + 落盘 last_refresh_ts */ reset_state(NOW_S - 13 * 3600); /* 13h 前刷过 → 已临期 */ int rc = account_cookie_maybe_refresh(); CHECK(rc == NCM_OK, "临期: rc=0 (got %d)", rc); CHECK(g_refresh_calls == 1, "临期: refresh 被调 1 次 (got %d)", g_refresh_calls); CHECK(g_set_cookie_calls == 1, "临期: 新 cookie 已应用落盘 (got %d)", g_set_cookie_calls); CHECK(strstr(g_applied_cookie, "NEW_TOKEN") != NULL, "临期: 应用的是新 MUSIC_U"); CHECK(g_st.cookie_last_refresh_ts == NOW_S, "临期: last_refresh_ts 更新为 now (got %lld)", (long long)g_st.cookie_last_refresh_ts); CHECK(strstr(g_account_json, "\"last_refresh_ts\":") != NULL, "临时: account.json 记录 last_refresh_ts"); CHECK(g_cfg_fail_ts == 0, "临期成功: 冷却已清 (got %d)", g_cfg_fail_ts); /* ② 未临期:年龄 ≤ 阈值 → 不刷 */ reset_state(NOW_S - 5 * 3600); /* 5h 前刷过 → 未临期 */ rc = account_cookie_maybe_refresh(); CHECK(rc == NCM_OK && g_refresh_calls == 0, "未临期: 不刷 (calls=%d)", g_refresh_calls); /* ③ 首次/旧会话:last_refresh_ts=0 → 视为临期刷一次 */ reset_state(0); rc = account_cookie_maybe_refresh(); CHECK(rc == NCM_OK && g_refresh_calls == 1, "旧会话(last=0): 视为临期刷一次 (calls=%d)", g_refresh_calls); /* ④ 失败退避:网络/TLS 瞬断 → 负错误码 + 冷却记录,随后冷却期内不重刷 */ reset_state(NOW_S - 13 * 3600); g_refresh_rc = NCM_ERR_NET; rc = account_cookie_maybe_refresh(); CHECK(rc == NCM_ERR_NET, "瞬断: rc=%d (期望 %d)", rc, NCM_ERR_NET); CHECK(g_set_cookie_calls == 0, "瞬断: 未应用新 cookie"); CHECK(g_cfg_fail_ts == (int)NOW_S, "瞬断: 冷却时间已持久化 (got %d)", g_cfg_fail_ts); /* 同 boot 再调(单飞)→ 不重刷;跨 boot 冷却:走新进程静态清零但 fail_ts 持久化在 config */ int c = g_refresh_calls; rc = account_cookie_maybe_refresh(); CHECK(rc == NCM_OK && g_refresh_calls == c, "瞬断+单飞: 本 boot 不再重试 (calls=%d)", g_refresh_calls); /* ⑤ 失效兜底:code301 → NCM_ERR_AUTH + 置「需回登录页」标志 */ reset_state(NOW_S - 13 * 3600); g_refresh_rc = NCM_ERR_AUTH; rc = account_cookie_maybe_refresh(); CHECK(rc == NCM_ERR_AUTH, "失效: rc=%d (期望 %d)", rc, NCM_ERR_AUTH); CHECK(account_cookie_need_relogin() == 1, "失效: 置回登录页标志"); account_cookie_relogin_consume(); CHECK(account_cookie_need_relogin() == 0, "失效: 标志可被 UI 消费"); /* ⑥ 冷却(跨 boot):config fail_ts 为 10 分钟前,即使临期也不刷 */ reset_state(NOW_S - 13 * 3600); g_cfg_fail_ts = (int)(NOW_S - 10 * 60); /* 上次失败 10 分钟前 < 30 分钟冷却 */ rc = account_cookie_maybe_refresh(); CHECK(rc == NCM_OK && g_refresh_calls == 0, "冷却(10min<30min): 不刷 (calls=%d)", g_refresh_calls); /* 冷却过期(>30min)后再刷 */ reset_state(NOW_S - 13 * 3600); g_cfg_fail_ts = (int)(NOW_S - 31 * 60); /* 31 分钟前 > 冷却 */ rc = account_cookie_maybe_refresh(); CHECK(rc == NCM_OK && g_refresh_calls == 1, "冷却过期(31min>30min): 重刷 (calls=%d)", g_refresh_calls); /* ⑦ 登录重置:mark_fresh 后年龄=now、冷却清、单飞清 → 不再临期(默认阈值内) */ reset_state(NOW_S - 13 * 3600); account_cookie_mark_fresh(); g_refresh_calls = 0; rc = account_cookie_maybe_refresh(); CHECK(rc == NCM_OK && g_refresh_calls == 0, "登录后 mark_fresh: 视为全新不刷 (calls=%d)", g_refresh_calls); printf("\n%s\n", g_fail ? "== RESULT: FAIL ==" : "== RESULT: PASS =="); return g_fail ? 1 : 0; }