tabletcommand-backend-models 7.4.111 → 7.4.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/constants.js +6 -1
- package/build/constants.js.map +1 -1
- package/build/index.js +6 -1
- package/build/index.js.map +1 -1
- package/build/models/data-sharing-agreement.js +161 -0
- package/build/models/data-sharing-agreement.js.map +1 -0
- package/build/models/department.js +17 -1
- package/build/models/department.js.map +1 -1
- package/build/models/drone-connection.js +101 -0
- package/build/models/drone-connection.js.map +1 -0
- package/build/models/user.js +4 -0
- package/build/models/user.js.map +1 -1
- package/build/test/0index.js +1 -0
- package/build/test/0index.js.map +1 -1
- package/build/test/data-sharing-agreement.js +220 -0
- package/build/test/data-sharing-agreement.js.map +1 -0
- package/build/test/department.js +5 -0
- package/build/test/department.js.map +1 -1
- package/build/test/drone-connection.js +152 -0
- package/build/test/drone-connection.js.map +1 -0
- package/build/test/mock.js +22 -0
- package/build/test/mock.js.map +1 -1
- package/build/test/user.js +10 -0
- package/build/test/user.js.map +1 -1
- package/build/types/data-sharing-agreement.js +8 -0
- package/build/types/data-sharing-agreement.js.map +1 -0
- package/build/types/drone-connection.js +3 -0
- package/build/types/drone-connection.js.map +1 -0
- package/cspell.json +1 -0
- package/definitions/constants.d.ts +4 -0
- package/definitions/constants.d.ts.map +1 -1
- package/definitions/index.d.ts +17 -2
- package/definitions/index.d.ts.map +1 -1
- package/definitions/models/data-sharing-agreement.d.ts +13 -0
- package/definitions/models/data-sharing-agreement.d.ts.map +1 -0
- package/definitions/models/department.d.ts +3 -0
- package/definitions/models/department.d.ts.map +1 -1
- package/definitions/models/drone-connection.d.ts +22 -0
- package/definitions/models/drone-connection.d.ts.map +1 -0
- package/definitions/models/user.d.ts.map +1 -1
- package/definitions/test/data-sharing-agreement.d.ts +2 -0
- package/definitions/test/data-sharing-agreement.d.ts.map +1 -0
- package/definitions/test/drone-connection.d.ts +2 -0
- package/definitions/test/drone-connection.d.ts.map +1 -0
- package/definitions/test/mock.d.ts +22 -0
- package/definitions/test/mock.d.ts.map +1 -1
- package/definitions/types/data-sharing-agreement.d.ts +30 -0
- package/definitions/types/data-sharing-agreement.d.ts.map +1 -0
- package/definitions/types/department.d.ts +4 -0
- package/definitions/types/department.d.ts.map +1 -1
- package/definitions/types/drone-connection.d.ts +19 -0
- package/definitions/types/drone-connection.d.ts.map +1 -0
- package/definitions/types/user.d.ts +1 -0
- package/definitions/types/user.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +5 -0
- package/src/index.ts +16 -0
- package/src/models/data-sharing-agreement.ts +182 -0
- package/src/models/department.ts +19 -0
- package/src/models/drone-connection.ts +121 -0
- package/src/models/user.ts +4 -0
- package/src/test/0index.ts +1 -0
- package/src/test/data-sharing-agreement.ts +238 -0
- package/src/test/department.ts +6 -0
- package/src/test/drone-connection.ts +164 -0
- package/src/test/mock.ts +23 -0
- package/src/test/user.ts +11 -0
- package/src/types/data-sharing-agreement.ts +49 -0
- package/src/types/department.ts +5 -0
- package/src/types/drone-connection.ts +21 -0
- package/src/types/user.ts +1 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { assert } from "chai";
|
|
2
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
3
|
+
import * as m from "../index";
|
|
4
|
+
import * as config from "./config";
|
|
5
|
+
import mockModule from "./mock";
|
|
6
|
+
|
|
7
|
+
describe("DataSharingAgreement", function() {
|
|
8
|
+
let models: m.BackendModels, mongoose: m.MongooseModule;
|
|
9
|
+
let testItem: ReturnType<typeof mockModule>["dataSharingAgreement"];
|
|
10
|
+
|
|
11
|
+
beforeEach(async function() {
|
|
12
|
+
const c = await m.connect(config.url);
|
|
13
|
+
models = c.models;
|
|
14
|
+
mongoose = c.mongoose;
|
|
15
|
+
testItem = mockModule({ mongoose }).dataSharingAgreement;
|
|
16
|
+
await models.DataSharingAgreement.deleteMany({});
|
|
17
|
+
});
|
|
18
|
+
afterEach(async function() {
|
|
19
|
+
await models.DataSharingAgreement.deleteMany({});
|
|
20
|
+
await mongoose.disconnect();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("is saved and reads back every field", async function() {
|
|
24
|
+
const sut = await new models.DataSharingAgreement(testItem).save();
|
|
25
|
+
|
|
26
|
+
assert.isNotNull(sut._id);
|
|
27
|
+
assert.equal(sut.id, sut._id.toHexString());
|
|
28
|
+
assert.isString(sut.uuid);
|
|
29
|
+
assert.isTrue(sut.uuid !== "");
|
|
30
|
+
|
|
31
|
+
assert.equal(sut.departmentId, testItem.departmentId);
|
|
32
|
+
assert.equal(sut.departmentName, testItem.departmentName);
|
|
33
|
+
assert.equal(sut.agencyId?.toHexString(), testItem.agencyId.toHexString());
|
|
34
|
+
assert.equal(sut.agencyName, testItem.agencyName);
|
|
35
|
+
assert.equal(sut.userId, testItem.userId);
|
|
36
|
+
assert.equal(sut.email, testItem.email);
|
|
37
|
+
assert.equal(sut.signerName, testItem.signerName);
|
|
38
|
+
|
|
39
|
+
assert.equal(sut.agreementTitle, testItem.agreementTitle);
|
|
40
|
+
assert.equal(sut.agreementVersion, testItem.agreementVersion);
|
|
41
|
+
assert.equal(sut.agreementText, testItem.agreementText);
|
|
42
|
+
assert.equal(sut.action, "signed");
|
|
43
|
+
assert.equal(sut.consents.length, 2);
|
|
44
|
+
assert.equal(sut.consents[0]?.key, "shareAVL");
|
|
45
|
+
assert.equal(sut.consents[0]?.label, testItem.consents[0]?.label);
|
|
46
|
+
assert.isTrue(sut.consents[0]?.checked);
|
|
47
|
+
assert.equal(sut.consents[1]?.key, "intterra");
|
|
48
|
+
assert.isFalse(sut.consents[1]?.checked);
|
|
49
|
+
|
|
50
|
+
assert.equal(sut.remoteAddress, testItem.remoteAddress);
|
|
51
|
+
assert.equal(sut.userAgent, testItem.userAgent);
|
|
52
|
+
assert.equal(sut.appVersion, testItem.appVersion);
|
|
53
|
+
assert.equal(sut.createdAt.toISOString(), testItem.createdAt.toISOString());
|
|
54
|
+
assert.instanceOf(sut.modified, Date);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("defaults optional fields", async function() {
|
|
58
|
+
const sut = await new models.DataSharingAgreement({
|
|
59
|
+
departmentId: "d1",
|
|
60
|
+
userId: "u1",
|
|
61
|
+
email: "u1@example.com",
|
|
62
|
+
agreementTitle: "Data Sharing Addendum",
|
|
63
|
+
agreementVersion: "2026-08-1",
|
|
64
|
+
agreementText: "text",
|
|
65
|
+
action: "revoked",
|
|
66
|
+
}).save();
|
|
67
|
+
|
|
68
|
+
assert.isString(sut.uuid);
|
|
69
|
+
assert.isTrue(sut.uuid !== "");
|
|
70
|
+
assert.equal(sut.departmentName, "");
|
|
71
|
+
assert.isNull(sut.agencyId);
|
|
72
|
+
assert.equal(sut.agencyName, "");
|
|
73
|
+
assert.equal(sut.signerName, "");
|
|
74
|
+
assert.deepEqual(sut.consents, []);
|
|
75
|
+
assert.equal(sut.remoteAddress, "");
|
|
76
|
+
assert.equal(sut.userAgent, "");
|
|
77
|
+
assert.equal(sut.appVersion, "");
|
|
78
|
+
assert.instanceOf(sut.createdAt, Date);
|
|
79
|
+
assert.instanceOf(sut.modified, Date);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("defaults consent label and checked", async function() {
|
|
83
|
+
const sut = await new models.DataSharingAgreement({
|
|
84
|
+
...testItem,
|
|
85
|
+
consents: [{ key: "firstArriving" }],
|
|
86
|
+
}).save();
|
|
87
|
+
|
|
88
|
+
assert.equal(sut.consents.length, 1);
|
|
89
|
+
assert.equal(sut.consents[0]?.key, "firstArriving");
|
|
90
|
+
assert.equal(sut.consents[0]?.label, "");
|
|
91
|
+
assert.isFalse(sut.consents[0]?.checked);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("requires the legal-record fields", async function() {
|
|
95
|
+
const required = [
|
|
96
|
+
"departmentId",
|
|
97
|
+
"userId",
|
|
98
|
+
"email",
|
|
99
|
+
"agreementTitle",
|
|
100
|
+
"agreementVersion",
|
|
101
|
+
"agreementText",
|
|
102
|
+
"action",
|
|
103
|
+
];
|
|
104
|
+
for (const field of required) {
|
|
105
|
+
const item: Record<string, unknown> = { ...testItem };
|
|
106
|
+
delete item[field];
|
|
107
|
+
try {
|
|
108
|
+
await new models.DataSharingAgreement(item).save();
|
|
109
|
+
assert.isFalse(true, `Expecting missing ${field} to fail validation.`);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
assert.match((error as Error).message, new RegExp(`${field}.*required`, "i"));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("rejects a consent without a key", async function() {
|
|
117
|
+
try {
|
|
118
|
+
await new models.DataSharingAgreement({
|
|
119
|
+
...testItem,
|
|
120
|
+
consents: [{ label: "No key", checked: true }],
|
|
121
|
+
}).save();
|
|
122
|
+
assert.isFalse(true, "Expecting consent without key to fail validation.");
|
|
123
|
+
} catch (error) {
|
|
124
|
+
assert.match((error as Error).message, /key.*required/i);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("only accepts known actions", async function() {
|
|
129
|
+
for (const action of m.DataSharingAgreementActions) {
|
|
130
|
+
const sut = await new models.DataSharingAgreement({ ...testItem, action }).save();
|
|
131
|
+
assert.equal(sut.action, action);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
await new models.DataSharingAgreement({ ...testItem, action: "deleted" }).save();
|
|
136
|
+
assert.isFalse(true, "Expecting unknown action to fail validation.");
|
|
137
|
+
} catch (error) {
|
|
138
|
+
assert.match((error as Error).message, /action/);
|
|
139
|
+
assert.match((error as Error).message, /enum/i);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("does not change record fields on a re-save", async function() {
|
|
144
|
+
const saved = await new models.DataSharingAgreement(testItem).save();
|
|
145
|
+
|
|
146
|
+
const loaded = await models.DataSharingAgreement.findById(saved._id);
|
|
147
|
+
assert.isNotNull(loaded);
|
|
148
|
+
if (!loaded) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
loaded.set({
|
|
152
|
+
departmentId: "other-department",
|
|
153
|
+
action: "revoked",
|
|
154
|
+
agreementText: "tampered",
|
|
155
|
+
agreementVersion: "9999-01-1",
|
|
156
|
+
consents: [],
|
|
157
|
+
remoteAddress: "10.0.0.1",
|
|
158
|
+
createdAt: new Date("2000-01-01T00:00:00.000Z"),
|
|
159
|
+
});
|
|
160
|
+
await loaded.save();
|
|
161
|
+
|
|
162
|
+
const reloaded = await models.DataSharingAgreement.findById(saved._id);
|
|
163
|
+
assert.isNotNull(reloaded);
|
|
164
|
+
assert.equal(reloaded?.departmentId, testItem.departmentId);
|
|
165
|
+
assert.equal(reloaded?.action, "signed");
|
|
166
|
+
assert.equal(reloaded?.agreementText, testItem.agreementText);
|
|
167
|
+
assert.equal(reloaded?.agreementVersion, testItem.agreementVersion);
|
|
168
|
+
assert.equal(reloaded?.consents.length, 2);
|
|
169
|
+
assert.equal(reloaded?.remoteAddress, testItem.remoteAddress);
|
|
170
|
+
assert.equal(reloaded?.createdAt.toISOString(), testItem.createdAt.toISOString());
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("keeps every record as history, newest first", async function() {
|
|
174
|
+
const signed = await new models.DataSharingAgreement({
|
|
175
|
+
...testItem,
|
|
176
|
+
action: "signed",
|
|
177
|
+
createdAt: new Date("2026-08-20T17:30:00.000Z"),
|
|
178
|
+
}).save();
|
|
179
|
+
const updated = await new models.DataSharingAgreement({
|
|
180
|
+
...testItem,
|
|
181
|
+
action: "updated",
|
|
182
|
+
consents: [
|
|
183
|
+
{ key: "shareAVL", label: "AVL Sharing (CAL FIRE Op Area)", checked: true },
|
|
184
|
+
{ key: "intterra", label: "Intterra", checked: true },
|
|
185
|
+
],
|
|
186
|
+
createdAt: new Date("2026-08-21T09:00:00.000Z"),
|
|
187
|
+
}).save();
|
|
188
|
+
const revoked = await new models.DataSharingAgreement({
|
|
189
|
+
...testItem,
|
|
190
|
+
action: "revoked",
|
|
191
|
+
consents: [
|
|
192
|
+
{ key: "shareAVL", label: "AVL Sharing (CAL FIRE Op Area)", checked: false },
|
|
193
|
+
{ key: "intterra", label: "Intterra", checked: false },
|
|
194
|
+
],
|
|
195
|
+
createdAt: new Date("2026-08-22T09:00:00.000Z"),
|
|
196
|
+
}).save();
|
|
197
|
+
// Another department must not leak into the history
|
|
198
|
+
await new models.DataSharingAgreement({
|
|
199
|
+
...testItem,
|
|
200
|
+
departmentId: "other-department",
|
|
201
|
+
}).save();
|
|
202
|
+
|
|
203
|
+
const history = await models.DataSharingAgreement.find({
|
|
204
|
+
departmentId: testItem.departmentId,
|
|
205
|
+
}).sort({ createdAt: -1 });
|
|
206
|
+
|
|
207
|
+
assert.equal(history.length, 3);
|
|
208
|
+
assert.equal(history[0]?.id, revoked.id);
|
|
209
|
+
assert.equal(history[1]?.id, updated.id);
|
|
210
|
+
assert.equal(history[2]?.id, signed.id);
|
|
211
|
+
assert.isFalse(history[0]?.consents.every((c) => c.checked));
|
|
212
|
+
assert.isTrue(history[1]?.consents.every((c) => c.checked));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("declares only the history index and no TTL index", async function() {
|
|
216
|
+
await models.DataSharingAgreement.syncIndexes();
|
|
217
|
+
const indexes = await models.DataSharingAgreement.collection.indexes();
|
|
218
|
+
|
|
219
|
+
const names = indexes.map((i) => i.name).sort();
|
|
220
|
+
assert.deepEqual(names, ["_id_", "departmentId_1_createdAt_-1"]);
|
|
221
|
+
|
|
222
|
+
const history = indexes.find((i) => i.name === "departmentId_1_createdAt_-1");
|
|
223
|
+
assert.deepEqual(history?.key, { departmentId: 1, createdAt: -1 });
|
|
224
|
+
|
|
225
|
+
for (const index of indexes) {
|
|
226
|
+
assert.isUndefined(index.expireAfterSeconds, `Unexpected TTL on ${index.name}`);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("serializes with id and without version key", async function() {
|
|
231
|
+
const sut = await new models.DataSharingAgreement(testItem).save();
|
|
232
|
+
const json = sut.toJSON() as Record<string, unknown>;
|
|
233
|
+
|
|
234
|
+
assert.equal(json.id, sut._id.toHexString());
|
|
235
|
+
assert.isUndefined(json.__v);
|
|
236
|
+
assert.equal(json.agreementText, testItem.agreementText);
|
|
237
|
+
});
|
|
238
|
+
});
|
package/src/test/department.ts
CHANGED
|
@@ -139,6 +139,12 @@ describe("Department", function () {
|
|
|
139
139
|
assert.isFalse(sut.watchDuty.visible);
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
it("defaults drones to disabled", async function () {
|
|
143
|
+
const item = new models.Department(testItem);
|
|
144
|
+
const sut = await item.save();
|
|
145
|
+
assert.isFalse(sut.drones.enabled);
|
|
146
|
+
});
|
|
147
|
+
|
|
142
148
|
it("saves samsara.tagIds and defaults it to an empty array", async function () {
|
|
143
149
|
const item = new models.Department({
|
|
144
150
|
...testItem,
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { assert } from "chai";
|
|
2
|
+
import { Types } from "mongoose";
|
|
3
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
4
|
+
import * as m from "../index";
|
|
5
|
+
import * as config from "./config";
|
|
6
|
+
|
|
7
|
+
describe("DroneConnection", function() {
|
|
8
|
+
let models: m.BackendModels, mongoose: m.MongooseModule;
|
|
9
|
+
let departmentId: Types.ObjectId;
|
|
10
|
+
|
|
11
|
+
beforeEach(async function() {
|
|
12
|
+
const c = await m.connect(config.url);
|
|
13
|
+
models = c.models;
|
|
14
|
+
mongoose = c.mongoose;
|
|
15
|
+
departmentId = new mongoose.Types.ObjectId();
|
|
16
|
+
await models.DroneConnection.syncIndexes();
|
|
17
|
+
});
|
|
18
|
+
afterEach(async function() {
|
|
19
|
+
await models.DroneConnection.deleteMany({});
|
|
20
|
+
await mongoose.disconnect();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("is saved and reads back every field", async function() {
|
|
24
|
+
const sut = await new models.DroneConnection({
|
|
25
|
+
departmentId,
|
|
26
|
+
provider: "skydio",
|
|
27
|
+
active: true,
|
|
28
|
+
label: "Skydio HQ",
|
|
29
|
+
authToken: { iv: "iv-token", encryptedData: "enc-token" },
|
|
30
|
+
authSecret: { iv: "iv-secret", encryptedData: "enc-secret" },
|
|
31
|
+
options: {
|
|
32
|
+
drones: "listed",
|
|
33
|
+
serials: ["SN-1", "SN-2"],
|
|
34
|
+
},
|
|
35
|
+
}).save();
|
|
36
|
+
|
|
37
|
+
assert.isNotNull(sut._id);
|
|
38
|
+
assert.equal(sut.departmentId.toString(), departmentId.toString());
|
|
39
|
+
assert.equal(sut.provider, "skydio");
|
|
40
|
+
assert.isTrue(sut.active);
|
|
41
|
+
assert.equal(sut.label, "Skydio HQ");
|
|
42
|
+
assert.equal(sut.authToken.iv, "iv-token");
|
|
43
|
+
assert.equal(sut.authToken.encryptedData, "enc-token");
|
|
44
|
+
assert.equal(sut.authSecret.iv, "iv-secret");
|
|
45
|
+
assert.equal(sut.authSecret.encryptedData, "enc-secret");
|
|
46
|
+
assert.equal(sut.options?.drones, "listed");
|
|
47
|
+
assert.deepEqual(sut.options?.serials, ["SN-1", "SN-2"]);
|
|
48
|
+
assert.instanceOf(sut.modified, Date);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("leaves options undefined when none are given", async function() {
|
|
52
|
+
const sut = await new models.DroneConnection({
|
|
53
|
+
departmentId,
|
|
54
|
+
provider: "skydio",
|
|
55
|
+
label: "No options",
|
|
56
|
+
authToken: { iv: "iv", encryptedData: "t" },
|
|
57
|
+
authSecret: { iv: "iv", encryptedData: "s" },
|
|
58
|
+
}).save();
|
|
59
|
+
|
|
60
|
+
assert.isUndefined(sut.options);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("rejects an unknown provider", async function() {
|
|
64
|
+
let thrownError: unknown = null;
|
|
65
|
+
try {
|
|
66
|
+
await new models.DroneConnection({
|
|
67
|
+
departmentId,
|
|
68
|
+
provider: "bogus",
|
|
69
|
+
label: "Bad provider",
|
|
70
|
+
}).save();
|
|
71
|
+
} catch (e) {
|
|
72
|
+
thrownError = e;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
assert.isNotNull(thrownError, "expected an unknown provider to be rejected");
|
|
76
|
+
assert.include(String((thrownError as Error).message), "provider");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("rejects an unknown options.drones value", async function() {
|
|
80
|
+
let thrownError: unknown = null;
|
|
81
|
+
try {
|
|
82
|
+
await new models.DroneConnection({
|
|
83
|
+
departmentId,
|
|
84
|
+
provider: "skydio",
|
|
85
|
+
label: "Bad options",
|
|
86
|
+
options: {
|
|
87
|
+
drones: "list",
|
|
88
|
+
serials: [],
|
|
89
|
+
},
|
|
90
|
+
}).save();
|
|
91
|
+
} catch (e) {
|
|
92
|
+
thrownError = e;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
assert.isNotNull(thrownError, "expected an unknown options.drones value to be rejected");
|
|
96
|
+
assert.include(String((thrownError as Error).message), "options.drones");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("stores no timestamp field outside the schema", async function() {
|
|
100
|
+
await new models.DroneConnection({
|
|
101
|
+
departmentId,
|
|
102
|
+
provider: "skydio",
|
|
103
|
+
label: "Raw read",
|
|
104
|
+
}).save();
|
|
105
|
+
|
|
106
|
+
const raw = await models.DroneConnection.collection.findOne({ departmentId });
|
|
107
|
+
assert.isNotNull(raw);
|
|
108
|
+
assert.notProperty(raw, "createdAt");
|
|
109
|
+
assert.notProperty(raw, "updatedAt");
|
|
110
|
+
assert.property(raw, "modified");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("advances modified on findOneAndUpdate", async function() {
|
|
114
|
+
const staleDate = new Date("2020-01-02T03:04:05.000Z");
|
|
115
|
+
await new models.DroneConnection({
|
|
116
|
+
departmentId,
|
|
117
|
+
provider: "skydio",
|
|
118
|
+
label: "Stale",
|
|
119
|
+
}).save();
|
|
120
|
+
await models.DroneConnection.collection.updateOne({
|
|
121
|
+
departmentId,
|
|
122
|
+
}, {
|
|
123
|
+
$set: {
|
|
124
|
+
modified: staleDate,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const updated = await models.DroneConnection.findOneAndUpdate({
|
|
129
|
+
departmentId,
|
|
130
|
+
}, {
|
|
131
|
+
$set: {
|
|
132
|
+
active: true,
|
|
133
|
+
},
|
|
134
|
+
}, {
|
|
135
|
+
new: true,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
assert.isNotNull(updated);
|
|
139
|
+
assert.isTrue(updated!.modified > staleDate, "expected modified to advance");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("enforces a unique (departmentId, provider, label) index", async function() {
|
|
143
|
+
await new models.DroneConnection({
|
|
144
|
+
departmentId,
|
|
145
|
+
provider: "skydio",
|
|
146
|
+
label: "Skydio HQ",
|
|
147
|
+
}).save();
|
|
148
|
+
|
|
149
|
+
let thrownError: unknown = null;
|
|
150
|
+
try {
|
|
151
|
+
await new models.DroneConnection({
|
|
152
|
+
departmentId,
|
|
153
|
+
provider: "skydio",
|
|
154
|
+
label: "Skydio HQ",
|
|
155
|
+
}).save();
|
|
156
|
+
} catch (e) {
|
|
157
|
+
thrownError = e;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
assert.isNotNull(thrownError, "expected a duplicate (departmentId, label) to be rejected");
|
|
161
|
+
assert.equal((thrownError as { code?: number }).code, 11000);
|
|
162
|
+
assert.include(String((thrownError as Error).message), "E11000");
|
|
163
|
+
});
|
|
164
|
+
});
|
package/src/test/mock.ts
CHANGED
|
@@ -1031,6 +1031,28 @@ export default function mockModule(dependencies: { mongoose: Mongoose; }) {
|
|
|
1031
1031
|
watchDutyModifiedAt: new Date("2025-08-02T11:00:00.000Z"),
|
|
1032
1032
|
};
|
|
1033
1033
|
|
|
1034
|
+
const dataSharingAgreement = {
|
|
1035
|
+
departmentId: "5195426cc4e016a988000965",
|
|
1036
|
+
departmentName: "Test Department",
|
|
1037
|
+
agencyId: new dependencies.mongoose.Types.ObjectId("5195426cc4e016a988000966"),
|
|
1038
|
+
agencyName: "Test Agency",
|
|
1039
|
+
userId: "5195426cc4e016a988000967",
|
|
1040
|
+
email: "signer@example.com",
|
|
1041
|
+
signerName: "Signer Person",
|
|
1042
|
+
agreementTitle: "Data Sharing Addendum",
|
|
1043
|
+
agreementVersion: "2026-08-1",
|
|
1044
|
+
agreementText: "By checking a vendor below you authorize Tablet Command to share this account's data with that vendor.",
|
|
1045
|
+
action: "signed",
|
|
1046
|
+
consents: [
|
|
1047
|
+
{ key: "shareAVL", label: "AVL Sharing (CAL FIRE Op Area)", checked: true },
|
|
1048
|
+
{ key: "intterra", label: "Intterra", checked: false },
|
|
1049
|
+
],
|
|
1050
|
+
remoteAddress: "203.0.113.10",
|
|
1051
|
+
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Safari/605.1.15",
|
|
1052
|
+
appVersion: "1.2.3",
|
|
1053
|
+
createdAt: new Date("2026-08-20T17:30:00.000Z"),
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1034
1056
|
const audioStreamAuthentication = {
|
|
1035
1057
|
_id: new mongoose.Types.ObjectId(),
|
|
1036
1058
|
name: "test-stream",
|
|
@@ -1685,6 +1707,7 @@ export default function mockModule(dependencies: { mongoose: Mongoose; }) {
|
|
|
1685
1707
|
checklist,
|
|
1686
1708
|
checklistItem,
|
|
1687
1709
|
csvImport,
|
|
1710
|
+
dataSharingAgreement,
|
|
1688
1711
|
department,
|
|
1689
1712
|
deviceMapping,
|
|
1690
1713
|
deviceMappingWithWhiteSpaces,
|
package/src/test/user.ts
CHANGED
|
@@ -51,4 +51,15 @@ describe("User", function () {
|
|
|
51
51
|
assert.equal(item.restrictedCommentsEnabled, true);
|
|
52
52
|
assert.isTrue(sut.canBlockIncidents);
|
|
53
53
|
});
|
|
54
|
+
|
|
55
|
+
it("saves and retrieves visibleSources", async function () {
|
|
56
|
+
const item = new models.User({
|
|
57
|
+
...testItem,
|
|
58
|
+
stealthStatus: m.UserStealthStatus.Hidden,
|
|
59
|
+
visibleSources: [m.LocationSource.VehicleModem, m.LocationSource.TCIOS],
|
|
60
|
+
});
|
|
61
|
+
const sut = await item.save();
|
|
62
|
+
assert.equal(sut.stealthStatus, "hidden");
|
|
63
|
+
assert.deepEqual(sut.visibleSources, [m.LocationSource.VehicleModem, m.LocationSource.TCIOS]);
|
|
64
|
+
});
|
|
54
65
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Types } from "mongoose";
|
|
2
|
+
|
|
3
|
+
// "signed" - first acceptance of the agreement text for this department
|
|
4
|
+
// "updated" - vendor checkbox state changed under an already-accepted agreement
|
|
5
|
+
// "revoked" - data sharing cancelled; the record captures the final (all-off) state
|
|
6
|
+
export const DataSharingAgreementActions = ["signed", "updated", "revoked"] as const;
|
|
7
|
+
export type DataSharingAgreementAction = typeof DataSharingAgreementActions[number];
|
|
8
|
+
|
|
9
|
+
// One vendor / config checkbox as it was shown to the signer.
|
|
10
|
+
// `checked` is the state AFTER the action this record describes.
|
|
11
|
+
export interface DataSharingVendorConsentType {
|
|
12
|
+
key: string,
|
|
13
|
+
label: string,
|
|
14
|
+
checked: boolean,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Append-only legal record: every signature, change and revocation is a new document.
|
|
18
|
+
// Nothing here is ever updated or deleted through the API - the history is the collection.
|
|
19
|
+
// Names and agreement text are denormalized on purpose so the record cannot drift from
|
|
20
|
+
// what the signer actually saw.
|
|
21
|
+
export interface DataSharingAgreementType {
|
|
22
|
+
_id: Types.ObjectId,
|
|
23
|
+
uuid: string,
|
|
24
|
+
|
|
25
|
+
// Who / where. "Account" in the ticket == Department; departmentId is a string
|
|
26
|
+
// (hex of Department._id) per codebase convention.
|
|
27
|
+
departmentId: string,
|
|
28
|
+
departmentName: string,
|
|
29
|
+
agencyId: Types.ObjectId | null,
|
|
30
|
+
agencyName: string,
|
|
31
|
+
userId: string,
|
|
32
|
+
email: string,
|
|
33
|
+
signerName: string,
|
|
34
|
+
|
|
35
|
+
// What they signed
|
|
36
|
+
agreementTitle: string,
|
|
37
|
+
agreementVersion: string,
|
|
38
|
+
agreementText: string,
|
|
39
|
+
action: DataSharingAgreementAction,
|
|
40
|
+
consents: DataSharingVendorConsentType[],
|
|
41
|
+
|
|
42
|
+
// Evidence captured server-side from the request
|
|
43
|
+
remoteAddress: string,
|
|
44
|
+
userAgent: string,
|
|
45
|
+
appVersion: string,
|
|
46
|
+
|
|
47
|
+
createdAt: Date,
|
|
48
|
+
modified: Date,
|
|
49
|
+
}
|
package/src/types/department.ts
CHANGED
|
@@ -359,6 +359,10 @@ export interface DroneSenseConfigurationType {
|
|
|
359
359
|
apiKey: string,
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
export interface DronesConfigurationType {
|
|
363
|
+
enabled: boolean,
|
|
364
|
+
}
|
|
365
|
+
|
|
362
366
|
export interface FireMapperConfigurationType {
|
|
363
367
|
enabled: boolean,
|
|
364
368
|
auth: FireMapperAuthV2Type,
|
|
@@ -707,6 +711,7 @@ export interface DepartmentType {
|
|
|
707
711
|
usft: USFTConfigurationType,
|
|
708
712
|
onestep: OneStepConfigurationType, // cspell:disable-line
|
|
709
713
|
dronesense: DroneSenseConfigurationType,
|
|
714
|
+
drones: DronesConfigurationType,
|
|
710
715
|
selfAssignmentEnabled: boolean,
|
|
711
716
|
shareAVL: AccountConfigurationShareAVL,
|
|
712
717
|
shareIncident: AccountConfigurationShareIncident,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Types } from "mongoose";
|
|
2
|
+
import { DroneProvider } from "../constants";
|
|
3
|
+
import { EncryptedDataType } from "./common";
|
|
4
|
+
|
|
5
|
+
export interface DroneConnectionOptionsType {
|
|
6
|
+
drones: "all" | "listed",
|
|
7
|
+
serials: string[],
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DroneConnectionType {
|
|
11
|
+
// The stable id the video gateway diffs on.
|
|
12
|
+
_id: Types.ObjectId,
|
|
13
|
+
departmentId: Types.ObjectId,
|
|
14
|
+
provider: DroneProvider,
|
|
15
|
+
active: boolean,
|
|
16
|
+
label: string,
|
|
17
|
+
authToken: EncryptedDataType, // Skydio token id
|
|
18
|
+
authSecret: EncryptedDataType, // Skydio token secret
|
|
19
|
+
options?: DroneConnectionOptionsType, // omitted means every drone on the account
|
|
20
|
+
modified: Date,
|
|
21
|
+
}
|
package/src/types/user.ts
CHANGED
|
@@ -139,6 +139,7 @@ export interface UserType {
|
|
|
139
139
|
shareWith3rdPartiesEnabled: boolean,
|
|
140
140
|
socketIO: PubNubTokenSchemaType,
|
|
141
141
|
stealthStatus: string, // UserStealthStatus
|
|
142
|
+
visibleSources: string[],
|
|
142
143
|
superuser: boolean,
|
|
143
144
|
superUserReadOnly: boolean
|
|
144
145
|
syncLoggingExpireDate: Date,
|