@remit/drizzle-service 0.0.1
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/drizzle.config.ts +15 -0
- package/package.json +52 -0
- package/src/db.ts +13 -0
- package/src/dialect.ts +13 -0
- package/src/error.ts +37 -0
- package/src/id.ts +50 -0
- package/src/index.ts +62 -0
- package/src/pagination.ts +29 -0
- package/src/repos/cascade-delete.sqlite.test.ts +159 -0
- package/src/repos/cascade-delete.test.ts +423 -0
- package/src/repos/cascade-delete.ts +219 -0
- package/src/repos/envelope.sqlite.test.ts +94 -0
- package/src/repos/envelope.test.ts +225 -0
- package/src/repos/envelope.ts +342 -0
- package/src/repos/filter-anchor.test.ts +131 -0
- package/src/repos/filter-anchor.ts +105 -0
- package/src/repos/filter.test.ts +279 -0
- package/src/repos/filter.ts +253 -0
- package/src/repos/i4-account-config.test.ts +105 -0
- package/src/repos/i4-account-config.ts +257 -0
- package/src/repos/i4-account-export-request.ts +141 -0
- package/src/repos/i4-account-setting.test.ts +94 -0
- package/src/repos/i4-account-setting.ts +92 -0
- package/src/repos/i4-account.test.ts +223 -0
- package/src/repos/i4-account.ts +380 -0
- package/src/repos/i4-address-wellknown.ts +31 -0
- package/src/repos/i4-address.test.ts +358 -0
- package/src/repos/i4-address.ts +613 -0
- package/src/repos/i4-mailbox-lock.test.ts +368 -0
- package/src/repos/i4-mailbox-lock.ts +140 -0
- package/src/repos/i4-mailbox-special-use.ts +165 -0
- package/src/repos/i4-mailbox.test.ts +188 -0
- package/src/repos/i4-mailbox.ts +347 -0
- package/src/repos/i4-message-flag-push.test.ts +189 -0
- package/src/repos/i4-message-flag-push.ts +173 -0
- package/src/repos/i4-message-placement-move.test.ts +135 -0
- package/src/repos/i4-message-placement-move.ts +144 -0
- package/src/repos/i4-organize-job-request.ts +142 -0
- package/src/repos/i4-outbox-message.test.ts +298 -0
- package/src/repos/i4-outbox-message.ts +299 -0
- package/src/repos/label.conformance.sqlite.test.ts +23 -0
- package/src/repos/label.conformance.test.ts +19 -0
- package/src/repos/label.ts +125 -0
- package/src/repos/mappers.ts +171 -0
- package/src/repos/message-flag.ts +162 -0
- package/src/repos/message-label.test.ts +110 -0
- package/src/repos/message-label.ts +96 -0
- package/src/repos/message.sqlite.test.ts +118 -0
- package/src/repos/message.test.ts +488 -0
- package/src/repos/message.ts +558 -0
- package/src/repos/serialized-writes.sqlite.test.ts +199 -0
- package/src/repos/test-helpers.ts +222 -0
- package/src/repos/thread-message.sqlite.test.ts +198 -0
- package/src/repos/thread-message.test.ts +744 -0
- package/src/repos/thread-message.ts +832 -0
- package/src/repos/thread-search-predicates.ts +79 -0
- package/src/repos/unit-of-work.sqlite.test.ts +138 -0
- package/src/repos/unit-of-work.test.ts +105 -0
- package/src/repos/unit-of-work.ts +31 -0
- package/src/schema/active-entities.ts +19 -0
- package/src/schema/i4-account-config.ts +4 -0
- package/src/schema/i4-account-export-request.ts +3 -0
- package/src/schema/i4-account-setting.ts +3 -0
- package/src/schema/i4-address.ts +3 -0
- package/src/schema/i4-mailbox-lock.ts +3 -0
- package/src/schema/i4-mailbox.ts +4 -0
- package/src/schema/i4-message-flag-push.ts +3 -0
- package/src/schema/i4-message-placement-move.ts +3 -0
- package/src/schema/i4-organize-job-request.ts +1 -0
- package/src/schema/i4-outbox-message.ts +3 -0
- package/src/schema/message-data.ts +44 -0
- package/src/schema/outbox.ts +74 -0
- package/src/schema/thread-message.ts +3 -0
- package/src/schema-full-sqlite.ts +11 -0
- package/src/schema-full.ts +16 -0
- package/src/schema.ts +28 -0
- package/src/sqlite-client.ts +52 -0
- package/src/test-db-sqlite.ts +75 -0
- package/src/test-db.ts +76 -0
- package/src/tx.ts +208 -0
- package/src/vps-migrations-drift.test.ts +34 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateOrganizeJobRequestInput,
|
|
3
|
+
IOrganizeJobRequestRepository,
|
|
4
|
+
OrganizeJobRequestItem,
|
|
5
|
+
ResultList,
|
|
6
|
+
UpdateOrganizeJobRequestInput,
|
|
7
|
+
} from "@remit/data-ports";
|
|
8
|
+
import { eq } from "drizzle-orm";
|
|
9
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
10
|
+
import { NotFoundError } from "../error.js";
|
|
11
|
+
import { randomId } from "../id.js";
|
|
12
|
+
import { decodeToken, resultList } from "../pagination.js";
|
|
13
|
+
import { organizeJobRequestTable } from "../schema/i4-organize-job-request.js";
|
|
14
|
+
|
|
15
|
+
type DB = NodePgDatabase<Record<string, unknown>>;
|
|
16
|
+
|
|
17
|
+
function rowToItem(
|
|
18
|
+
row: typeof organizeJobRequestTable.$inferSelect,
|
|
19
|
+
): OrganizeJobRequestItem {
|
|
20
|
+
return {
|
|
21
|
+
organizeJobId: row.organizeJobId,
|
|
22
|
+
accountConfigId: row.accountConfigId,
|
|
23
|
+
userId: row.userId,
|
|
24
|
+
state: row.state as OrganizeJobRequestItem["state"],
|
|
25
|
+
anchorMessageId: row.anchorMessageId,
|
|
26
|
+
matchOperator: row.matchOperator as OrganizeJobRequestItem["matchOperator"],
|
|
27
|
+
literalClauses:
|
|
28
|
+
row.literalClauses as OrganizeJobRequestItem["literalClauses"],
|
|
29
|
+
similarityThreshold: row.similarityThreshold,
|
|
30
|
+
actionLabelId: row.actionLabelId,
|
|
31
|
+
actionMailboxId: row.actionMailboxId,
|
|
32
|
+
matchedCount: row.matchedCount,
|
|
33
|
+
appliedCount: row.appliedCount,
|
|
34
|
+
failedCount: row.failedCount,
|
|
35
|
+
errorMessage: row.errorMessage,
|
|
36
|
+
ttl: row.ttl,
|
|
37
|
+
createdAt: row.createdAt,
|
|
38
|
+
updatedAt: row.updatedAt,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class OrganizeJobRequestRepo implements IOrganizeJobRequestRepository {
|
|
43
|
+
constructor(private db: DB) {}
|
|
44
|
+
|
|
45
|
+
async create(
|
|
46
|
+
input: CreateOrganizeJobRequestInput,
|
|
47
|
+
): Promise<OrganizeJobRequestItem> {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
const [row] = await this.db
|
|
50
|
+
.insert(organizeJobRequestTable)
|
|
51
|
+
.values({
|
|
52
|
+
organizeJobId: randomId(),
|
|
53
|
+
accountConfigId: input.accountConfigId,
|
|
54
|
+
userId: input.userId,
|
|
55
|
+
state: input.state ?? "Pending",
|
|
56
|
+
anchorMessageId: input.anchorMessageId,
|
|
57
|
+
matchOperator: input.matchOperator,
|
|
58
|
+
literalClauses: input.literalClauses,
|
|
59
|
+
similarityThreshold: input.similarityThreshold,
|
|
60
|
+
actionLabelId: input.actionLabelId,
|
|
61
|
+
actionMailboxId: input.actionMailboxId,
|
|
62
|
+
matchedCount: 0,
|
|
63
|
+
appliedCount: 0,
|
|
64
|
+
failedCount: 0,
|
|
65
|
+
errorMessage: "",
|
|
66
|
+
ttl: input.ttl,
|
|
67
|
+
createdAt: now,
|
|
68
|
+
updatedAt: now,
|
|
69
|
+
})
|
|
70
|
+
.returning();
|
|
71
|
+
return rowToItem(row);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async get(organizeJobId: string): Promise<OrganizeJobRequestItem> {
|
|
75
|
+
const [row] = await this.db
|
|
76
|
+
.select()
|
|
77
|
+
.from(organizeJobRequestTable)
|
|
78
|
+
.where(eq(organizeJobRequestTable.organizeJobId, organizeJobId));
|
|
79
|
+
if (!row)
|
|
80
|
+
throw new NotFoundError(`OrganizeJobRequest not found: ${organizeJobId}`);
|
|
81
|
+
return rowToItem(row);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async update(
|
|
85
|
+
organizeJobId: string,
|
|
86
|
+
input: UpdateOrganizeJobRequestInput,
|
|
87
|
+
): Promise<OrganizeJobRequestItem> {
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const updates: Partial<typeof organizeJobRequestTable.$inferInsert> = {
|
|
90
|
+
updatedAt: now,
|
|
91
|
+
};
|
|
92
|
+
if (input.state !== undefined) updates.state = input.state;
|
|
93
|
+
if (input.matchedCount !== undefined)
|
|
94
|
+
updates.matchedCount = input.matchedCount;
|
|
95
|
+
if (input.appliedCount !== undefined)
|
|
96
|
+
updates.appliedCount = input.appliedCount;
|
|
97
|
+
if (input.failedCount !== undefined)
|
|
98
|
+
updates.failedCount = input.failedCount;
|
|
99
|
+
if (input.errorMessage !== undefined)
|
|
100
|
+
updates.errorMessage = input.errorMessage;
|
|
101
|
+
|
|
102
|
+
const [row] = await this.db
|
|
103
|
+
.update(organizeJobRequestTable)
|
|
104
|
+
.set(updates)
|
|
105
|
+
.where(eq(organizeJobRequestTable.organizeJobId, organizeJobId))
|
|
106
|
+
.returning();
|
|
107
|
+
if (!row)
|
|
108
|
+
throw new NotFoundError(`OrganizeJobRequest not found: ${organizeJobId}`);
|
|
109
|
+
return rowToItem(row);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async listByAccountConfig(
|
|
113
|
+
accountConfigId: string,
|
|
114
|
+
options?: { limit?: number; continuationToken?: string },
|
|
115
|
+
): Promise<ResultList<OrganizeJobRequestItem>> {
|
|
116
|
+
const limit = options?.limit ?? 100;
|
|
117
|
+
|
|
118
|
+
if (options?.continuationToken) {
|
|
119
|
+
decodeToken(options.continuationToken);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const rows = await this.db
|
|
123
|
+
.select()
|
|
124
|
+
.from(organizeJobRequestTable)
|
|
125
|
+
.where(eq(organizeJobRequestTable.accountConfigId, accountConfigId))
|
|
126
|
+
.limit(limit + 1);
|
|
127
|
+
|
|
128
|
+
const hasMore = rows.length > limit;
|
|
129
|
+
const items = rows.slice(0, limit).map(rowToItem);
|
|
130
|
+
const lastItem = items[items.length - 1];
|
|
131
|
+
return resultList(
|
|
132
|
+
items,
|
|
133
|
+
limit,
|
|
134
|
+
hasMore && lastItem
|
|
135
|
+
? {
|
|
136
|
+
createdAt: lastItem.createdAt,
|
|
137
|
+
organizeJobId: lastItem.organizeJobId,
|
|
138
|
+
}
|
|
139
|
+
: undefined,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { after, before, describe, test } from "node:test";
|
|
3
|
+
import { ForbiddenError, NotFoundError } from "../error.js";
|
|
4
|
+
import { createTestDb, randomId, type TestDb } from "../test-db.js";
|
|
5
|
+
import { OutboxMessageRepo } from "./i4-outbox-message.js";
|
|
6
|
+
|
|
7
|
+
function makeOutboxInput(accountId: string, accountConfigId: string) {
|
|
8
|
+
return {
|
|
9
|
+
accountId,
|
|
10
|
+
accountConfigId,
|
|
11
|
+
fromAddress: "from@example.com",
|
|
12
|
+
toAddresses: ["to@example.com"],
|
|
13
|
+
messageIdValue: `<${randomId()}@example.com>`,
|
|
14
|
+
status: "queued" as const,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("OutboxMessageRepo", () => {
|
|
19
|
+
let db: TestDb;
|
|
20
|
+
let close: () => Promise<void>;
|
|
21
|
+
let repo: OutboxMessageRepo;
|
|
22
|
+
|
|
23
|
+
before(async () => {
|
|
24
|
+
({ db, close } = await createTestDb());
|
|
25
|
+
repo = new OutboxMessageRepo(db as never);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
after(async () => {
|
|
29
|
+
await close();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("create and get", async () => {
|
|
33
|
+
const accountId = randomId();
|
|
34
|
+
const accountConfigId = randomId();
|
|
35
|
+
const msg = await repo.create(makeOutboxInput(accountId, accountConfigId));
|
|
36
|
+
|
|
37
|
+
assert.ok(msg.outboxMessageId);
|
|
38
|
+
assert.equal(msg.accountId, accountId);
|
|
39
|
+
assert.equal(msg.status, "queued");
|
|
40
|
+
assert.deepEqual(msg.ccAddresses, []);
|
|
41
|
+
assert.deepEqual(msg.bccAddresses, []);
|
|
42
|
+
assert.deepEqual(msg.references, []);
|
|
43
|
+
|
|
44
|
+
const fetched = await repo.get(accountConfigId, msg.outboxMessageId);
|
|
45
|
+
assert.equal(fetched.outboxMessageId, msg.outboxMessageId);
|
|
46
|
+
|
|
47
|
+
await repo.delete(accountConfigId, msg.outboxMessageId);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("batchGet WHERE id = ANY($1)", async () => {
|
|
51
|
+
const accountId = randomId();
|
|
52
|
+
const accountConfigId = randomId();
|
|
53
|
+
const m1 = await repo.create(makeOutboxInput(accountId, accountConfigId));
|
|
54
|
+
const m2 = await repo.create(makeOutboxInput(accountId, accountConfigId));
|
|
55
|
+
|
|
56
|
+
const results = await repo.get(accountConfigId, [
|
|
57
|
+
m1.outboxMessageId,
|
|
58
|
+
m2.outboxMessageId,
|
|
59
|
+
]);
|
|
60
|
+
assert.equal(results.length, 2);
|
|
61
|
+
|
|
62
|
+
await repo.deleteMany(accountConfigId, [
|
|
63
|
+
m1.outboxMessageId,
|
|
64
|
+
m2.outboxMessageId,
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("deleteMany only removes ids owned by the accountConfigId", async () => {
|
|
69
|
+
const accountConfigId = randomId();
|
|
70
|
+
const other = randomId();
|
|
71
|
+
const mine = await repo.create(
|
|
72
|
+
makeOutboxInput(randomId(), accountConfigId),
|
|
73
|
+
);
|
|
74
|
+
const theirs = await repo.create(makeOutboxInput(randomId(), other));
|
|
75
|
+
|
|
76
|
+
// A foreign tenant cannot delete an id it does not own.
|
|
77
|
+
await repo.deleteMany(other, [mine.outboxMessageId]);
|
|
78
|
+
assert.ok(await repo.get(accountConfigId, mine.outboxMessageId));
|
|
79
|
+
|
|
80
|
+
// The owner's call removes only its own id; the other tenant's row survives.
|
|
81
|
+
await repo.deleteMany(accountConfigId, [
|
|
82
|
+
mine.outboxMessageId,
|
|
83
|
+
theirs.outboxMessageId,
|
|
84
|
+
]);
|
|
85
|
+
await assert.rejects(() => repo.get(accountConfigId, mine.outboxMessageId));
|
|
86
|
+
assert.ok(await repo.get(other, theirs.outboxMessageId));
|
|
87
|
+
|
|
88
|
+
await repo.delete(other, theirs.outboxMessageId);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("batchGet empty array returns []", async () => {
|
|
92
|
+
const results = await repo.get(randomId(), []);
|
|
93
|
+
assert.deepEqual(results, []);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("updateStatus changes status", async () => {
|
|
97
|
+
const accountConfigId = randomId();
|
|
98
|
+
const msg = await repo.create(makeOutboxInput(randomId(), accountConfigId));
|
|
99
|
+
const updated = await repo.updateStatus(
|
|
100
|
+
accountConfigId,
|
|
101
|
+
msg.outboxMessageId,
|
|
102
|
+
"sending",
|
|
103
|
+
);
|
|
104
|
+
assert.equal(updated.status, "sending");
|
|
105
|
+
await repo.delete(accountConfigId, msg.outboxMessageId);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("markSent clears lastError and lastSmtpCode", async () => {
|
|
109
|
+
const accountConfigId = randomId();
|
|
110
|
+
const msg = await repo.create({
|
|
111
|
+
...makeOutboxInput(randomId(), accountConfigId),
|
|
112
|
+
lastError: "previous error",
|
|
113
|
+
lastSmtpCode: 550,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const sentAt = Date.now();
|
|
117
|
+
const updated = await repo.markSent(accountConfigId, msg.outboxMessageId, {
|
|
118
|
+
sentAt,
|
|
119
|
+
smtpMessageId: "<smtp-id@example.com>",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
assert.equal(updated.status, "sent");
|
|
123
|
+
assert.equal(updated.sentAt, sentAt);
|
|
124
|
+
assert.equal(updated.lastError, undefined);
|
|
125
|
+
assert.equal(updated.lastSmtpCode, undefined);
|
|
126
|
+
|
|
127
|
+
await repo.delete(accountConfigId, msg.outboxMessageId);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("listByAccount returns messages in desc order", async () => {
|
|
131
|
+
const accountId = randomId();
|
|
132
|
+
const accountConfigId = randomId();
|
|
133
|
+
const m1 = await repo.create(makeOutboxInput(accountId, accountConfigId));
|
|
134
|
+
const m2 = await repo.create(makeOutboxInput(accountId, accountConfigId));
|
|
135
|
+
|
|
136
|
+
const list = await repo.listByAccount(accountId);
|
|
137
|
+
assert.ok(list.items.length >= 2);
|
|
138
|
+
// Most recent first
|
|
139
|
+
const ids = list.items.map((m) => m.outboxMessageId);
|
|
140
|
+
assert.ok(ids.includes(m1.outboxMessageId));
|
|
141
|
+
assert.ok(ids.includes(m2.outboxMessageId));
|
|
142
|
+
|
|
143
|
+
await repo.deleteMany(accountConfigId, [
|
|
144
|
+
m1.outboxMessageId,
|
|
145
|
+
m2.outboxMessageId,
|
|
146
|
+
]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("listByAccount paginates without dupes, gaps, or non-termination", async () => {
|
|
150
|
+
const accountId = randomId();
|
|
151
|
+
const accountConfigId = randomId();
|
|
152
|
+
const created: string[] = [];
|
|
153
|
+
for (let i = 0; i < 5; i++) {
|
|
154
|
+
const msg = await repo.create(
|
|
155
|
+
makeOutboxInput(accountId, accountConfigId),
|
|
156
|
+
);
|
|
157
|
+
created.push(msg.outboxMessageId);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const seen: string[] = [];
|
|
161
|
+
let continuationToken: string | undefined;
|
|
162
|
+
let pages = 0;
|
|
163
|
+
do {
|
|
164
|
+
const page = await repo.listByAccount(accountId, {
|
|
165
|
+
limit: 2,
|
|
166
|
+
continuationToken,
|
|
167
|
+
});
|
|
168
|
+
seen.push(...page.items.map((m) => m.outboxMessageId));
|
|
169
|
+
continuationToken = page.continuationToken;
|
|
170
|
+
pages++;
|
|
171
|
+
assert.ok(pages < 10, "pagination must terminate");
|
|
172
|
+
} while (continuationToken);
|
|
173
|
+
|
|
174
|
+
assert.equal(seen.length, 5, "every row returned exactly once");
|
|
175
|
+
assert.equal(new Set(seen).size, 5, "no duplicates across pages");
|
|
176
|
+
assert.deepEqual([...seen].sort(), [...created].sort(), "no gaps");
|
|
177
|
+
|
|
178
|
+
await repo.deleteMany(accountConfigId, created);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("listQueued returns only queued messages", async () => {
|
|
182
|
+
const accountId = randomId();
|
|
183
|
+
const accountConfigId = randomId();
|
|
184
|
+
const queued = await repo.create(
|
|
185
|
+
makeOutboxInput(accountId, accountConfigId),
|
|
186
|
+
);
|
|
187
|
+
const sent = await repo.create({
|
|
188
|
+
...makeOutboxInput(accountId, accountConfigId),
|
|
189
|
+
status: "sent",
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const list = await repo.listQueued(accountId);
|
|
193
|
+
const ids = list.map((m) => m.outboxMessageId);
|
|
194
|
+
assert.ok(ids.includes(queued.outboxMessageId));
|
|
195
|
+
assert.equal(ids.includes(sent.outboxMessageId), false);
|
|
196
|
+
|
|
197
|
+
await repo.deleteMany(accountConfigId, [
|
|
198
|
+
queued.outboxMessageId,
|
|
199
|
+
sent.outboxMessageId,
|
|
200
|
+
]);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("cross-tenant: get refuses a foreign accountConfig", async () => {
|
|
204
|
+
const accountConfigId = randomId();
|
|
205
|
+
const other = randomId();
|
|
206
|
+
const msg = await repo.create(makeOutboxInput(randomId(), accountConfigId));
|
|
207
|
+
|
|
208
|
+
await assert.rejects(
|
|
209
|
+
() => repo.get(other, msg.outboxMessageId),
|
|
210
|
+
/OutboxMessage not found/,
|
|
211
|
+
);
|
|
212
|
+
assert.deepEqual(await repo.get(other, [msg.outboxMessageId]), []);
|
|
213
|
+
const owned = await repo.get(accountConfigId, [msg.outboxMessageId]);
|
|
214
|
+
assert.equal(owned.length, 1);
|
|
215
|
+
|
|
216
|
+
await repo.delete(accountConfigId, msg.outboxMessageId);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("cross-tenant: get mode 'act' denies a foreign accountConfig with Forbidden, not NotFound", async () => {
|
|
220
|
+
const accountConfigId = randomId();
|
|
221
|
+
const other = randomId();
|
|
222
|
+
const msg = await repo.create(makeOutboxInput(randomId(), accountConfigId));
|
|
223
|
+
|
|
224
|
+
await assert.rejects(
|
|
225
|
+
() => repo.get(other, msg.outboxMessageId, "read"),
|
|
226
|
+
(err: unknown) => err instanceof NotFoundError,
|
|
227
|
+
);
|
|
228
|
+
await assert.rejects(
|
|
229
|
+
() => repo.get(other, msg.outboxMessageId, "act"),
|
|
230
|
+
(err: unknown) => err instanceof ForbiddenError,
|
|
231
|
+
);
|
|
232
|
+
await assert.rejects(
|
|
233
|
+
() => repo.get(other, `${msg.outboxMessageId}-missing`, "act"),
|
|
234
|
+
(err: unknown) => err instanceof NotFoundError,
|
|
235
|
+
"a truly absent id stays NotFound even in act mode",
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
await repo.delete(accountConfigId, msg.outboxMessageId);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("cross-tenant: update refuses a foreign accountConfig and leaves the row unchanged", async () => {
|
|
242
|
+
const accountConfigId = randomId();
|
|
243
|
+
const other = randomId();
|
|
244
|
+
const msg = await repo.create(makeOutboxInput(randomId(), accountConfigId));
|
|
245
|
+
|
|
246
|
+
await assert.rejects(
|
|
247
|
+
() => repo.update(other, msg.outboxMessageId, { subject: "hijacked" }),
|
|
248
|
+
/OutboxMessage not found/,
|
|
249
|
+
);
|
|
250
|
+
const still = await repo.get(accountConfigId, msg.outboxMessageId);
|
|
251
|
+
assert.equal(still.subject, undefined);
|
|
252
|
+
|
|
253
|
+
await repo.delete(accountConfigId, msg.outboxMessageId);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("cross-tenant: updateStatus refuses a foreign accountConfig and leaves the row unchanged", async () => {
|
|
257
|
+
const accountConfigId = randomId();
|
|
258
|
+
const other = randomId();
|
|
259
|
+
const msg = await repo.create(makeOutboxInput(randomId(), accountConfigId));
|
|
260
|
+
|
|
261
|
+
await assert.rejects(
|
|
262
|
+
() => repo.updateStatus(other, msg.outboxMessageId, "sending"),
|
|
263
|
+
/OutboxMessage not found/,
|
|
264
|
+
);
|
|
265
|
+
const still = await repo.get(accountConfigId, msg.outboxMessageId);
|
|
266
|
+
assert.equal(still.status, "queued");
|
|
267
|
+
|
|
268
|
+
await repo.delete(accountConfigId, msg.outboxMessageId);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("cross-tenant: markSent refuses a foreign accountConfig and leaves the row unchanged", async () => {
|
|
272
|
+
const accountConfigId = randomId();
|
|
273
|
+
const other = randomId();
|
|
274
|
+
const msg = await repo.create(makeOutboxInput(randomId(), accountConfigId));
|
|
275
|
+
|
|
276
|
+
await assert.rejects(
|
|
277
|
+
() => repo.markSent(other, msg.outboxMessageId, { sentAt: Date.now() }),
|
|
278
|
+
/OutboxMessage not found/,
|
|
279
|
+
);
|
|
280
|
+
const still = await repo.get(accountConfigId, msg.outboxMessageId);
|
|
281
|
+
assert.equal(still.status, "queued");
|
|
282
|
+
assert.equal(still.sentAt, undefined);
|
|
283
|
+
|
|
284
|
+
await repo.delete(accountConfigId, msg.outboxMessageId);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("cross-tenant: delete is a no-op for a foreign accountConfig", async () => {
|
|
288
|
+
const accountConfigId = randomId();
|
|
289
|
+
const other = randomId();
|
|
290
|
+
const msg = await repo.create(makeOutboxInput(randomId(), accountConfigId));
|
|
291
|
+
|
|
292
|
+
await repo.delete(other, msg.outboxMessageId);
|
|
293
|
+
const still = await repo.get(accountConfigId, msg.outboxMessageId);
|
|
294
|
+
assert.equal(still.outboxMessageId, msg.outboxMessageId);
|
|
295
|
+
|
|
296
|
+
await repo.delete(accountConfigId, msg.outboxMessageId);
|
|
297
|
+
});
|
|
298
|
+
});
|