diff --git a/laf-cloud/functions/userData.ts b/laf-cloud/functions/userData.ts index c0e75f5..65d6ede 100644 --- a/laf-cloud/functions/userData.ts +++ b/laf-cloud/functions/userData.ts @@ -46,7 +46,8 @@ export default async function (ctx: FunctionContext) { } //console.log(data); let res2 = await db.collection(dbname).where({ _id: uid }).update(data); - if (res2.ok && res2.updated == 1) { + // 同值保存不会增加 updated;匹配到用户且操作成功也属于保存成功。 + if (res2.ok && (res2.updated == 1 || res2.matched == 1)) { return { code: 1, data: null, msg: "用户数据更新成功" } } else { return { code: 0, data: null, msg: "用户数据更新失败" } diff --git a/laf-cloud/tests/user-data-save.test.mjs b/laf-cloud/tests/user-data-save.test.mjs new file mode 100644 index 0000000..b68021f --- /dev/null +++ b/laf-cloud/tests/user-data-save.test.mjs @@ -0,0 +1,120 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { registerHooks } from "node:module"; + +const state = { user: null, failUpdate: false, loseMatch: false, legacyResult: false, results: [] }; +const profile = { useravatar: "2", username: "player", useravatarIcon: "1", userCat: "3" }; + +globalThis.__userDataSaveCloudMock = { + database() { + return { + collection() { + return { + where(query) { + return { + async getOne() { + return { data: state.user?._id === query._id ? structuredClone(state.user) : null }; + }, + async update(payload) { + if (state.loseMatch) state.user = null; + const matched = state.user?._id === query._id ? 1 : 0; + const updated = !state.failUpdate && matched + && Object.entries(payload).some(([key, value]) => state.user[key] !== value) ? 1 : 0; + if (updated) Object.assign(state.user, payload); + const result = { ok: !state.failUpdate, updated, ...(!state.legacyResult ? { matched } : {}) }; + state.results.push(result); + return result; + }, + }; + }, + }; + }, + }; + }, +}; +globalThis.__userDataSaveUtilsMock = { + checkToken(received, expected) { return received === expected; }, +}; + +registerHooks({ + resolve(specifier, context, nextResolve) { + if (specifier === "@lafjs/cloud") { + return { url: "data:text/javascript,export default globalThis.__userDataSaveCloudMock", shortCircuit: true }; + } + if (specifier === "@/Utils") { + return { url: "data:text/javascript,export default globalThis.__userDataSaveUtilsMock", shortCircuit: true }; + } + return nextResolve(specifier, context); + }, +}); + +const { default: userData } = await import(new URL("../functions/userData.ts", import.meta.url)); + +function reset() { + state.user = { _id: "user-1", token: "token-1", useravatar: "0", username: "user", useravatarIcon: "0", userCat: "1" }; + state.failUpdate = false; + state.loseMatch = false; + state.legacyResult = false; + state.results = []; +} + +function save(extra = {}) { + return userData({ body: { + uid: "user-1", token: "token-1", action: "save", userData: JSON.stringify(profile), ...extra, + } }); +} + +test("saving changed and then identical profile data both succeeds", async () => { + reset(); + const first = await save(); + const repeated = await save(); + assert.deepEqual(state.results, [ + { ok: true, matched: 1, updated: 1 }, + { ok: true, matched: 1, updated: 0 }, + ]); + assert.equal(first.code, 1); + assert.equal(repeated.code, 1); + assert.deepEqual(state.user, { _id: "user-1", token: "token-1", ...profile }); +}); + +test("a missing uid is rejected without updating", async () => { + reset(); + assert.equal((await save({ uid: undefined })).code, 0); + assert.equal(state.results.length, 0); +}); + +test("an unknown user is not mistaken for an unchanged successful save", async () => { + reset(); + const before = structuredClone(state.user); + assert.equal((await save({ uid: "missing-user" })).code, 0); + assert.deepEqual(state.user, before); +}); + +test("losing the matching user during the update remains a failure", async () => { + reset(); + state.loseMatch = true; + assert.equal((await save()).code, 0); + assert.deepEqual(state.results, [{ ok: true, matched: 0, updated: 0 }]); +}); + +test("an explicitly failed database update remains a failure", async () => { + reset(); + state.failUpdate = true; + const before = structuredClone(state.user); + assert.equal((await save()).code, 0); + assert.deepEqual(state.results, [{ ok: false, matched: 1, updated: 0 }]); + assert.deepEqual(state.user, before); +}); + +test("an invalid token prevents all writes", async () => { + reset(); + assert.equal((await save({ token: "wrong-token" })).code, 0); + assert.equal(state.results.length, 0); +}); + +test("legacy update results with only ok and updated still succeed", async () => { + reset(); + state.legacyResult = true; + assert.equal((await save()).code, 1); + assert.deepEqual(state.results, [{ ok: true, updated: 1 }]); +});