import events from "node:events"; import assert from "node:assert/strict"; import * as lib from "@clusterio/lib"; import fs from "node:fs/promises"; import path from "node:path"; import "../setup_logging.js"; class MockDatastoreProvider extends lib.DatastoreProvider { constructor() { super(); this.methodCalls = []; this.value = new Map(); this.methodCalls.push(["constructor"]); } async save(data) { this.methodCalls.push(["save", data]); } async load() { this.methodCalls.push(["load"]); return this.value; } } class MockDatastore extends lib.Datastore { getData() { return this.data; // Protected member } callTouch(updates = [0]) { this.touch(updates); // Protected method } } describe("lib/datastore", function() { const baseDir = path.join("temp", "test", "datastore"); const testFiles = path.join("test", "file"); before(async function() { await fs.rm(baseDir, { force: true, recursive: true, maxRetries: 10 }); await fs.mkdir(baseDir, { recursive: true }); }); describe("class DatastoreProvider", function() { let datastoreProvider; beforeEach(function() { datastoreProvider = new MockDatastoreProvider(); }); it("can be constructed", function() { assert.deepEqual(datastoreProvider.methodCalls[0], ["constructor"]); }); }); describe("class MemoryDatastoreProvider", function() { let datastoreProvider, initialData; beforeEach(function() { initialData = new Map([ ["foo", "fooValue"], ["bar", "barValue"], ["baz", "bazValue"], ]); datastoreProvider = new lib.MemoryDatastoreProvider(initialData); }); describe("constructor", function() { it("has a default value", async function() { const value = await new lib.MemoryDatastoreProvider().load(); assert.notEqual(value, undefined); assert.notEqual(value, null); }); it("can have a value given", async function() { const expectedValue = new Map([["foo", "bar"]]); const value = await new lib.MemoryDatastoreProvider(expectedValue).load(); assert.deepEqual(value, expectedValue); }); }); describe("save", function() { it("can save data", async function() { const newData = new Map([["foo2", "fooValue2"]]); await datastoreProvider.save(newData); const loadedData = await datastoreProvider.load(); assert.deepEqual(loadedData, newData); }); }); describe("load", function() { it("can load data", async function() { const loadedData = await datastoreProvider.load(); assert.deepEqual(loadedData, initialData); }); }); }); describe("class JsonDatastoreProvider", function() { let datastoreProvider, initialData, dataPath; before(async function() { dataPath = path.join(baseDir, "data.json"); initialData = Object.entries(JSON.parse(await fs.readFile( path.join(testFiles, "json", "load_map.json"), { encoding: "utf8" } ))); }); beforeEach(async function() { await fs.copyFile(path.join(testFiles, "json", "load_map.json"), dataPath); datastoreProvider = new lib.JsonDatastoreProvider(dataPath); }); describe("constructor", function() { it("has default values", async function() { datastoreProvider = new lib.JsonDatastoreProvider(dataPath); const value = await datastoreProvider.load(); assert.deepEqual(value, new Map(initialData)); }); it("can accept a fromJson method", async function() { datastoreProvider = new lib.JsonDatastoreProvider( dataPath, json => [json] ); const value = await datastoreProvider.load(); assert.deepEqual(value, new Map(initialData.map(([k, v]) => [k, [v]]))); }); it("can accept a migration method", async function() { datastoreProvider = new lib.JsonDatastoreProvider( dataPath, json => json, // Nop rawJson => ({ c: "foo", ...rawJson }) ); const value = await datastoreProvider.load(); assert.deepEqual(value, new Map([...initialData, ["c", "foo"]])); }); it("can accept a finalise method", async function() { datastoreProvider = new lib.JsonDatastoreProvider( dataPath, json => json, // Nop rawJson => rawJson, // Nop obj => [obj] ); const value = await datastoreProvider.load(); assert.deepEqual(value, new Map(initialData.map(([k, v]) => [k, [v]]))); }); }); describe("save", function() { it("can save data", async function() { const newData = new Map([["foo", "bar"]]); await datastoreProvider.save(newData); const loadedData = await datastoreProvider.load(); assert.deepEqual(loadedData, newData); }); }); describe("load", function() { it("can load data", async function() { const loadedData = await datastoreProvider.load(); assert.deepEqual(loadedData, new Map(initialData)); }); it("has a default value", async function() { datastoreProvider = new lib.JsonDatastoreProvider(path.join(baseDir, "not_exists.json")); const loadedData = await datastoreProvider.load(); assert.equal(loadedData.size, 0); }); it("throws for invalid file contents", async function() { await fs.copyFile(path.join(testFiles, "json", "invalid.json"), dataPath); await assert.rejects(() => datastoreProvider.load(dataPath)); }); it("writes a backup if size changes after load", async function() { datastoreProvider = new lib.JsonDatastoreProvider( dataPath, json => json, // Nop rawJson => rawJson, // Nop () => undefined, // Throw away all data ); const entriesBefore = await fs.readdir(baseDir); const loadedData = await datastoreProvider.load(); const entriesAfter = await fs.readdir(baseDir); assert.equal(entriesAfter.length, entriesBefore.length + 1, "Backup was not created"); assert.equal(loadedData.size, 0); }); }); }); describe("class JsonIdDatastoreProvider", function() { let datastoreProvider, initialData, dataPath; before(async function() { dataPath = path.join(baseDir, "data.json"); initialData = JSON.parse(await fs.readFile( path.join(testFiles, "json", "load_array_map.json"), { encoding: "utf8" } )).map(v => [v.id, v]); }); beforeEach(async function() { await fs.copyFile(path.join(testFiles, "json", "load_array_map.json"), dataPath); datastoreProvider = new lib.JsonIdDatastoreProvider(dataPath); }); describe("constructor", function() { it("has default values", async function() { datastoreProvider = new lib.JsonIdDatastoreProvider(dataPath); const value = await datastoreProvider.load(); assert.deepEqual(value, new Map(initialData)); }); it("can accept a fromJson method", async function() { datastoreProvider = new lib.JsonIdDatastoreProvider( dataPath, json => ({...json, foo: true}) ); const value = await datastoreProvider.load(); assert.deepEqual(value, new Map(initialData.map(v => ([v[0], {...v[1], foo: true}])))); }); it("can accept a migration method", async function() { datastoreProvider = new lib.JsonIdDatastoreProvider( dataPath, json => json, // Nop rawJson => ([{ id: "c" }, ...rawJson]) ); const value = await datastoreProvider.load(); assert.deepEqual(value, new Map([...initialData, ["c", { id: "c" }]])); }); it("can accept a finalise method", async function() { datastoreProvider = new lib.JsonIdDatastoreProvider( dataPath, json => json, // Nop rawJson => rawJson, // Nop obj => ({...obj, foo: true}) ); const value = await datastoreProvider.load(); assert.deepEqual(value, new Map(initialData.map(v => ([v[0], {...v[1], foo: true}])))); }); }); describe("save", function() { it("can save data", async function() { const newData = new Map([["foo", { id: "foo" }]]); await datastoreProvider.save(newData); const loadedData = await datastoreProvider.load(); assert.deepEqual(loadedData, newData); }); }); describe("load", function() { it("can load data", async function() { const loadedData = await datastoreProvider.load(); assert.equal(loadedData.size, initialData.length); assert.deepEqual(loadedData, new Map(initialData)); }); it("has a default value", async function() { datastoreProvider = new lib.JsonIdDatastoreProvider(path.join(baseDir, "not_exists.json")); const loadedData = await datastoreProvider.load(); assert.equal(loadedData.size, 0); }); it("throws for invalid file contents", async function() { await fs.copyFile(path.join(testFiles, "json", "invalid.json"), dataPath); await assert.rejects(() => datastoreProvider.load(dataPath)); }); it("writes a backup if size changes after load", async function() { datastoreProvider = new lib.JsonIdDatastoreProvider( dataPath, json => json, // Nop rawJson => rawJson, // Nop () => undefined, // Throw away all data ); const entriesBefore = await fs.readdir(baseDir); const loadedData = await datastoreProvider.load(); const entriesAfter = await fs.readdir(baseDir); assert.equal(entriesAfter.length, entriesBefore.length + 1, "Backup was not created"); assert.equal(loadedData.size, 0); }); }); }); describe("class Datastore", function() { let datastoreProvider, datastore; beforeEach(function() { datastoreProvider = new MockDatastoreProvider(); datastoreProvider.value = new Map([ ["foo", "fooValue"], ["bar", "barValue"], ["baz", "bazValue"], ]); datastore = new MockDatastore(datastoreProvider, datastoreProvider.value); }); describe("constructor", function() { it("has a default value", function() { datastore = new MockDatastore(datastoreProvider); assert.deepEqual(datastore.getData(), new Map()); }); it("accepts a starting value", function() { datastore = new MockDatastore(datastoreProvider, datastoreProvider.value); assert.deepEqual(datastore.getData(), datastoreProvider.value); }); }); describe("getFilePath", function() { it("returns the correct filepath", function() { const mockConfig = { get(field) { if (field === "controller.database_directory") { return "databaseDir"; } throw new Error(`field ${field} not implemented`); }, }; assert.equal( lib.Datastore.getFilePath(mockConfig, "db1.json"), path.resolve("databaseDir", "db1.json") ); assert.equal( lib.Datastore.getFilePath(mockConfig, "db2.txt"), path.resolve("databaseDir", "db2.txt") ); assert.equal( lib.Datastore.getFilePath(mockConfig, path.join("foo", "db3.json")), path.resolve("databaseDir", "foo", "db3.json") ); }); }); describe("save", function() { it("should not save when clean", async function() { await datastore.save(); assert.equal(datastoreProvider.methodCalls.length, 1); }); it("should save when dirty", async function() { datastore.callTouch(); // Make the data dirty await datastore.save(); assert.equal(datastoreProvider.methodCalls.length, 2); assert.deepEqual(datastoreProvider.methodCalls[1], ["save", datastore.getData()]); }); it("should not save multiple times", async function() { datastore.callTouch(); // Make the data dirty await datastore.save(); assert.equal(datastoreProvider.methodCalls.length, 2); assert.deepEqual(datastoreProvider.methodCalls[1], ["save", datastore.getData()]); await datastore.save(); assert.equal(datastoreProvider.methodCalls.length, 2); }); it("should write overlapping saves in order with the latest data", async function() { const written = []; let release; datastoreProvider.save = async data => { const keys = [...data.keys()]; if (!written.length) { await new Promise(resolve => { release = resolve; }); } written.push(keys); }; datastore = new MockDatastore(datastoreProvider, new Map([["a", 1]])); datastore.callTouch(); const first = datastore.save(); await new Promise(resolve => setImmediate(resolve)); // First write in progress datastore.getData().set("b", 2); datastore.callTouch(); const second = datastore.save(); release(); await Promise.all([first, second]); assert.deepEqual(written, [["a"], ["a", "b"]]); }); it("should save again after a failed save", async function() { let fail = true; datastoreProvider.save = async () => { if (fail) { fail = false; throw new Error("disk full"); } datastoreProvider.methodCalls.push(["save"]); }; datastore.callTouch(); await assert.rejects(datastore.save(), /disk full/); await datastore.save(); assert.deepEqual(datastoreProvider.methodCalls.at(-1), ["save"]); }); }); describe("load", function() { it("should load new data", async function() { datastoreProvider.value = new Map(); assert.notEqual(datastore.getData(), datastoreProvider.value); await datastore.load(); assert.equal(datastoreProvider.methodCalls.length, 2); assert.deepEqual(datastoreProvider.methodCalls[1], ["load"]); assert.deepEqual(datastore.getData(), datastoreProvider.value); }); it("should not save after a load", async function() { datastore.callTouch(); // Make the data dirty await datastore.load(); assert.equal(datastoreProvider.methodCalls.length, 2); assert.deepEqual(datastoreProvider.methodCalls[1], ["load"]); await datastore.save(); assert.equal(datastoreProvider.methodCalls.length, 2); }); }); describe("touch", function() { it("should emit update events", function() { const eventsRaised = []; datastore.on("update", updates => eventsRaised.push(updates)); let updates = ["foo"]; datastore.callTouch(updates); assert.equal(eventsRaised.length, 1); assert.deepEqual(eventsRaised[0], updates); updates = ["foo", "bar"]; datastore.callTouch(updates); assert.equal(eventsRaised.length, 2); assert.deepEqual(eventsRaised[1], updates); }); }); describe("has", function() { it("returns true correctly", function() { assert.equal(datastore.has("foo"), true); assert.equal(datastore.has("bar"), true); assert.equal(datastore.has("baz"), true); }); it("returns false correctly", function() { assert.equal(datastore.has("missing1"), false); assert.equal(datastore.has("missing2"), false); assert.equal(datastore.has(1), false); assert.equal(datastore.has(2), false); }); }); describe("get", function() { it("returns the correct value", function() { assert.equal(datastore.get("foo"), "fooValue"); assert.equal(datastore.get("bar"), "barValue"); assert.equal(datastore.get("baz"), "bazValue"); }); it("returns undefined correctly", function() { assert.equal(datastore.get("missing1"), undefined); assert.equal(datastore.get("missing2"), undefined); assert.equal(datastore.get(1), undefined); assert.equal(datastore.get(2), undefined); }); }); describe("getMutable", function() { it("returns the correct value", function() { assert.equal(datastore.getMutable("foo"), "fooValue"); assert.equal(datastore.getMutable("bar"), "barValue"); assert.equal(datastore.getMutable("baz"), "bazValue"); }); it("returns undefined correctly", function() { assert.equal(datastore.getMutable("missing1"), undefined); assert.equal(datastore.getMutable("missing2"), undefined); assert.equal(datastore.getMutable(1), undefined); assert.equal(datastore.getMutable(2), undefined); }); }); describe("size", function() { it("returns the correct size", function() { assert.equal(datastore.size, 3); }); }); describe("iterator", function() { it("iterates all values", function() { const entries = []; for (const entry of datastore) { entries.push(entry); } assert.deepEqual(entries, [...datastoreProvider.value.entries()]); }); }); describe("values", function() { it("returns all values", function() { assert.deepEqual([...datastore.values()], [...datastoreProvider.value.values()]); }); }); describe("valuesMutable", function() { it("returns all values", function() { assert.deepEqual([...datastore.valuesMutable()], [...datastoreProvider.value.values()]); }); }); }); describe("class KeyValueDatastore", function() { let datastoreProvider, datastore, eventsRaised; beforeEach(function() { eventsRaised = []; datastoreProvider = new MockDatastoreProvider(); datastoreProvider.value = new Map([ ["foo", "fooValue"], ["bar", "barValue"], ["baz", "bazValue"], ]); datastore = new lib.KeyValueDatastore(datastoreProvider, datastoreProvider.value); datastore.on("update", updates => eventsRaised.push(updates)); }); describe("fromProvider", function() { it("loads data from the provider", async function() { datastore = await lib.KeyValueDatastore.fromProvider(datastoreProvider); assert(datastore instanceof lib.KeyValueDatastore); assert.equal(datastore.get("foo"), "fooValue"); assert.deepEqual(datastoreProvider.methodCalls[1], ["load"]); }); }); describe("set", function() { it("can set a value", function() { datastore.set("test", "testValue"); assert.equal(datastore.get("test"), "testValue"); assert.equal(eventsRaised.length, 1); assert.deepEqual(eventsRaised[0], [["test", "testValue"]]); }); it("allows saving after a value is set", async function() { datastore.set("test", "testValue"); await datastore.save(); assert.equal(datastoreProvider.methodCalls.length, 2); assert.deepEqual(datastoreProvider.methodCalls[1], ["save", new Map([...datastoreProvider.value.entries(), ["test", "testValue"]])] ); }); }); describe("setMany", function() { it("does not save with zero changes", async function() { datastore.setMany([]); await datastore.save(); assert.equal(datastoreProvider.methodCalls.length, 1); }); it("can set many values at once", async function() { const updates = [["test1", "testValue1"], ["test2", "testValue2"]]; datastore.setMany(updates); assert.equal(datastore.get("test1"), "testValue1"); assert.equal(datastore.get("test2"), "testValue2"); await datastore.save(); assert.equal(eventsRaised.length, 1); assert.deepEqual(eventsRaised[0], updates); assert.equal(datastoreProvider.methodCalls.length, 2); assert.deepEqual(datastoreProvider.methodCalls[1], ["save", new Map([...datastoreProvider.value.entries(), ...updates])] ); }); }); describe("delete", function() { it("can delete a value", function() { datastore.delete("foo"); assert.equal(datastore.get("foo"), undefined); assert.equal(eventsRaised.length, 1); assert.deepEqual(eventsRaised[0], [["foo", "fooValue", true]]); }); it("allows saving after a value is deleted", async function() { datastore.delete("foo"); await datastore.save(); assert.equal(datastoreProvider.methodCalls.length, 2); assert.deepEqual(datastoreProvider.methodCalls[1], ["save", new Map([...datastoreProvider.value.entries()].filter((v, k) => k !== "foo"))] ); }); it("does nothing when the key does not exist", function() { datastore.delete("test"); assert.equal(eventsRaised.length, 0); }); }); describe("deleteMany", function() { it("does not save with zero changes", async function() { datastore.deleteMany([]); await datastore.save(); assert.equal(datastoreProvider.methodCalls.length, 1); }); it("can delete many values at once", async function() { const keys = ["foo", "bar"]; datastore.deleteMany(keys); assert.equal(datastore.get("foo"), undefined); assert.equal(datastore.get("bar"), undefined); await datastore.save(); assert.equal(eventsRaised.length, 1); assert.deepEqual(eventsRaised[0], [["foo", "fooValue", true], ["bar", "barValue", true]]); assert.equal(datastoreProvider.methodCalls.length, 2); assert.deepEqual(datastoreProvider.methodCalls[1], ["save", new Map([...datastoreProvider.value.entries()].filter((v, k) => !keys.includes(k)))] ); }); }); }); describe("class SubscribableDatastore", function() { let datastoreProvider, datastore, eventsRaised; beforeEach(function() { eventsRaised = []; datastoreProvider = new MockDatastoreProvider(); datastoreProvider.value = new Map([ ["foo", {id: "foo", updatedAtMs: 10}], ["bar", {id: "bar"}], ["baz", {id: "baz"}], ]); datastore = new lib.SubscribableDatastore(datastoreProvider, datastoreProvider.value); datastore.on("update", updates => eventsRaised.push(updates)); }); describe("fromProvider", function() { it("loads data from the provider", async function() { datastore = await lib.SubscribableDatastore.fromProvider(datastoreProvider); assert(datastore instanceof lib.SubscribableDatastore); assert.equal(datastore.get("foo").id, "foo"); assert.equal(datastore.lastUpdatedAtMs, 10); assert.deepEqual(datastoreProvider.methodCalls[1], ["load"]); }); }); function forceFuture() { const future = {id: "future"}; for (let i = 1; i < 100; i++) { datastore.set(future); } return future.updatedAtMs; } describe("lastUpdateAtMs", function() { it("is set to the highest value after constructing a new instance", function() { assert(datastore.lastUpdatedAtMs === 10, `${datastore.lastUpdatedAtMs} !== 10`); }); it("is set to the highest value after loading from a provider", async function() { const future = Date.now() + 2000; datastore.provider.value = ["future", {id: "future", updatedAtMs: future}]; await datastore.load(); assert(datastore.lastUpdatedAtMs === future, `${datastore.lastUpdatedAtMs} !== ${future}`); }); }); describe("set", function() { it("can set a value", function() { const value = { id: "test" }; datastore.set(value); assert.equal(datastore.get(value.id), value); assert.equal(eventsRaised.length, 1); assert.deepEqual(eventsRaised[0], [value]); }); it("allows saving after a value is set", async function() { const value = { id: "test" }; datastore.set(value); await datastore.save(); assert.equal(datastoreProvider.methodCalls.length, 2); assert.deepEqual(datastoreProvider.methodCalls[1], ["save", new Map([...datastoreProvider.value.entries(), [value.id, value]])] ); }); it("sets the updatedAtMs property", async function() { const value = { id: "test" }; datastore.set(value); assert.notEqual(value.updatedAtMs, undefined); assert(value.updatedAtMs > 0, "updatedAtMs not greater than zero"); }); it("increments the updatedAtMs property", async function() { const value = { id: "test" }; datastore.set(value); const prev = value.updatedAtMs; datastore.set(value); assert(value.updatedAtMs > prev, "updatedAtMs must increase"); }); it("increments the updatedAtMs property past existing values", async function() { const future = forceFuture(); const value = { id: "test" }; datastore.set(value); assert(value.updatedAtMs > future, "updatedAtMs must increase"); }); }); describe("setMany", function() { it("does not save with zero changes", async function() { datastore.setMany([]); await datastore.save(); assert.equal(datastoreProvider.methodCalls.length, 1); }); it("can set many values at once", async function() { const updates = [{ id: "test1" }, { id: "test2" }]; datastore.setMany(updates); assert.equal(datastore.get(updates[0].id), updates[0]); assert.equal(datastore.get(updates[1].id), updates[1]); await datastore.save(); assert.equal(eventsRaised.length, 1); assert.deepEqual(eventsRaised[0], updates); assert.equal(datastoreProvider.methodCalls.length, 2); assert.deepEqual(datastoreProvider.methodCalls[1], ["save", new Map([...datastoreProvider.value.entries(), ...updates.map(v => [v.id, v])])] ); }); it("sets the updatedAtMs property", async function() { const updates = [{ id: "test1" }, { id: "test2" }]; datastore.setMany(updates); assert.notEqual(updates[0].updatedAtMs, undefined); assert.notEqual(updates[1].updatedAtMs, undefined); assert(updates[0].updatedAtMs > 0, "updatedAtMs greater than zero"); assert(updates[1].updatedAtMs > 0, "updatedAtMs greater than zero"); }); it("increments the updatedAtMs property", async function() { const value = { id: "test1" }; datastore.set(value); const prev = value.updatedAtMs; const updates = [{ id: "test1" }, { id: "test2" }]; datastore.setMany(updates); assert(updates[0].updatedAtMs > prev, "updatedAtMs must increase"); assert(updates[1].updatedAtMs > prev, "updatedAtMs must increase"); }); it("increments the updatedAtMs property past existing values", async function() { const future = forceFuture(); const updates = [{ id: "test1" }, { id: "test2" }]; datastore.setMany(updates); assert(updates[0].updatedAtMs > future, "updatedAtMs must increase"); assert(updates[1].updatedAtMs > future, "updatedAtMs must increase"); }); }); describe("delete", function() { it("can delete a value", function() { const value = datastore.get("foo"); datastore.delete(value); assert.equal(datastore.get(value.id), undefined); assert.equal(eventsRaised.length, 1); assert.deepEqual(eventsRaised[0], [value]); }); it("allows saving after a value is deleted", async function() { const value = datastore.get("foo"); datastore.delete(value); await datastore.save(); assert.equal(datastoreProvider.methodCalls.length, 2); assert.deepEqual(datastoreProvider.methodCalls[1], ["save", new Map([...datastoreProvider.value.entries()].filter((v, k) => k !== value.id))] ); }); it("does not break when the key does not exist", function() { const value = { id: "test" }; datastore.delete(value); assert.equal(eventsRaised.length, 1); assert.deepEqual(eventsRaised[0], [value]); }); it("sets the updatedAtMs property", function() { const value = { id: "test" }; datastore.delete(value); assert.notEqual(value.updatedAtMs, undefined); assert(value.updatedAtMs > 0, "updatedAtMs not greater than zero"); }); it("increments the updatedAtMs property", function() { const value = { id: "test" }; datastore.set(value); const prev = value.updatedAtMs; datastore.delete(value); assert(value.updatedAtMs > prev, "updatedAtMs must increase"); }); it("increments the updatedAtMs property past existing values", function() { const future = forceFuture(); const value = { id: "test" }; datastore.delete(value); assert(value.updatedAtMs > future, "updatedAtMs must increase"); }); it("sets the isDeleted property", function() { const value = { id: "test" }; datastore.delete(value); assert.equal(value.isDeleted, true); }); }); describe("deleteMany", function() { it("does not save with zero changes", async function() { datastore.deleteMany([]); await datastore.save(); assert.equal(datastoreProvider.methodCalls.length, 1); }); it("can delete many values at once", async function() { const values = [{id: "foo"}, {id: "bar"}]; datastore.deleteMany(values); assert.equal(datastore.get(values[0].id), undefined); assert.equal(datastore.get(values[1].id), undefined); await datastore.save(); assert.equal(eventsRaised.length, 1); assert.deepEqual(eventsRaised[0], values); assert.equal(datastoreProvider.methodCalls.length, 2); assert.deepEqual(datastoreProvider.methodCalls[1], ["save", new Map([...datastoreProvider.value.entries()] .filter((_, k) => !values.some(v => v.id === k)))] ); }); it("sets the updatedAtMs property", function() { const values = [{id: "foo"}, {id: "bar"}]; datastore.deleteMany(values); assert.notEqual(values[0].updatedAtMs, undefined); assert.notEqual(values[1].updatedAtMs, undefined); assert(values[0].updatedAtMs > 0, "updatedAtMs greater than zero"); assert(values[1].updatedAtMs > 0, "updatedAtMs greater than zero"); }); it("increments the updatedAtMs property", function() { const value = { id: "foo" }; datastore.set(value); const prev = value.updatedAtMs; const values = [{id: "foo", updatedAtMs: prev}, {id: "bar", updatedAtMs: prev}]; datastore.deleteMany(values); assert(values[0].updatedAtMs > prev, "updatedAtMs must increase"); assert(values[1].updatedAtMs > prev, "updatedAtMs must increase"); }); it("increments the updatedAtMs property past existing values", function() { const future = forceFuture(); const values = [{id: "foo"}, {id: "bar"}]; datastore.deleteMany(values); assert(values[0].updatedAtMs > future, "updatedAtMs must increase"); assert(values[1].updatedAtMs > future, "updatedAtMs must increase"); }); it("sets the isDeleted property", function() { const values = [{id: "foo"}, {id: "bar"}]; datastore.deleteMany(values); assert.equal(values[0].isDeleted, true); assert.equal(values[1].isDeleted, true); }); }); }); });