Files
factorio-clusterio/test/lib/datastore.js
T
Hornwitser b0595a6502 Fix identical updatedAtMs values in datastore
The assumption with the subscription system is that new updates will
always increment the updatedAtMs value to something greater than what
has been seen before.  This might not happen if the system clock is
adjusted, or updates are sent so fast that Date.now() returns the same
value.

Fix these edge cases so that the timestamp is always incremented.
2026-03-09 17:41:11 +01:00

777 lines
27 KiB
JavaScript

"use strict";
const events = require("events");
const assert = require("assert").strict;
const lib = require("@clusterio/lib");
const fs = require("fs-extra");
const path = require("path");
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.emptyDir(baseDir);
});
describe("class DatastoreProvider", function() {
let datastoreProvider;
beforeEach(function() {
datastoreProvider = new MockDatastoreProvider();
});
it("can be constructed", function() {
assert.deepEqual(datastoreProvider.methodCalls[0], ["constructor"]);
});
it("can be bootstrapped", async function() {
const rtn = await datastoreProvider.bootstrap();
assert.equal(rtn[0], datastoreProvider);
assert.equal(rtn[1], datastoreProvider.value);
assert.deepEqual(datastoreProvider.methodCalls[1], ["load"]);
});
});
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().bootstrap();
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).bootstrap();
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.copy(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.bootstrap();
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.bootstrap();
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.bootstrap();
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.bootstrap();
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.copy(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.copy(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.bootstrap();
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.bootstrap();
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.bootstrap();
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.bootstrap();
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.copy(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);
});
it("accepts the return of bootstrap", async function() {
datastore = new MockDatastore(...await datastoreProvider.bootstrap());
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);
});
});
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("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));
});
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);
});
});
});
});