@wssto2/vue-core 0.11.0 → 0.12.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/CHANGELOG.md +25 -0
- package/README.md +26 -0
- package/bin/architecture/engine.mjs +225 -0
- package/bin/architecture/packs.mjs +107 -0
- package/bin/cli.mjs +57 -0
- package/bin/commands/check-architecture.mjs +38 -0
- package/bin/commands/e2e-timings.mjs +58 -0
- package/bin/commands/feature.mjs +179 -0
- package/bin/commands/lint.mjs +532 -0
- package/bin/config.mjs +72 -0
- package/bin/templates/feature-crud/api.ts.tpl +35 -0
- package/bin/templates/feature-crud/components/__FEATURE_ENTITY__Modal.vue.tpl +36 -0
- package/bin/templates/feature-crud/form.ts.tpl +6 -0
- package/bin/templates/feature-crud/index.ts.tpl +7 -0
- package/bin/templates/feature-crud/routes.ts.tpl +13 -0
- package/bin/templates/feature-crud/tests/scaffold.test.ts.tpl +9 -0
- package/bin/templates/feature-crud/types.ts.tpl +3 -0
- package/bin/templates/feature-crud/views/Index.vue.tpl +36 -0
- package/bin/vue-core.mjs +4 -0
- package/dist/config/index.d.ts +97 -0
- package/dist/config/index.js +6 -0
- package/dist/eslint/index.d.ts +28 -0
- package/dist/eslint/index.js +87 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/modules/identity/entities.d.ts +3 -2
- package/dist/playwright/access.d.ts +61 -0
- package/dist/playwright/envelope.d.ts +37 -0
- package/dist/playwright/index.d.ts +13 -0
- package/dist/playwright/index.js +403 -0
- package/dist/playwright/notifications.d.ts +48 -0
- package/dist/playwright/profile.d.ts +35 -0
- package/dist/playwright/request.d.ts +14 -0
- package/dist/playwright/session.d.ts +48 -0
- package/dist/playwright/users.d.ts +57 -0
- package/package.json +48 -2
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
const u = (n, s) => ({ success: !0, data: n, ...s === void 0 ? {} : { meta: s } }), te = (n, s = {}) => u(n, { total: n.length, page: 1, per_page: 20, last_page: 1, from: n.length === 0 ? 0 : 1, to: n.length, ...s }), O = (n, s, e = {}) => u({ data: n, total: n.length, per_page: 20, current_page: 1, last_page: 1, from: n.length ? 1 : 0, to: n.length, ...s ? { meta: { views: s } } : {}, ...e }), a = (n, s = 200) => ({ status: s, contentType: "application/json", body: JSON.stringify(n) }), w = () => ({ status: 204, body: "" }), R = (n) => ({ status: 200, contentType: "text/event-stream", body: n.map((s) => `data: ${JSON.stringify(s)}
|
|
2
|
+
|
|
3
|
+
`).join("") }), j = (n, s, e = {}) => a({ success: !1, error: s, code: s, ...e.params === void 0 ? {} : { params: e.params }, ...e.field ? { fields: { [e.field]: s } } : {} }, n), z = "2099-01-01T00:00:00.000Z", Z = { id: 1, login: "admin", name: "Test Admin", email: "admin@test.com", locale: "en" };
|
|
4
|
+
function M(n, s = {}, e = { level: "organization" }) {
|
|
5
|
+
const i = e.id === void 0 ? { level: e.level } : { level: e.level, id: e.id };
|
|
6
|
+
return {
|
|
7
|
+
subject: { kind: "user", id: 1 },
|
|
8
|
+
root: e.level === "organization",
|
|
9
|
+
permissions: Object.fromEntries(n.map((o) => [o, { scope: i, qualifier: s[o] ?? "all", clauses: [] }]))
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function F(n = [], s = {}) {
|
|
13
|
+
return { user: Z, expires_at: z, navigation: [], access: M(n), ...s };
|
|
14
|
+
}
|
|
15
|
+
const J = (n = [], s = {}) => u(F(n, s));
|
|
16
|
+
async function ae(n, s = [], e = {}) {
|
|
17
|
+
await n.route(
|
|
18
|
+
"**/api/v1/auth/me",
|
|
19
|
+
(i) => i.fulfill(a(J(s, { user: { ...Z, ...e.user }, navigation: e.navigation ?? [], access: M(s, e.access, e.scope) })))
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
function S(n) {
|
|
23
|
+
const s = n.request(), e = new URL(s.url()), i = s.method(), o = i === "GET" || !s.postData() ? void 0 : s.postDataJSON();
|
|
24
|
+
return { method: i, url: e, path: e.pathname.replace(/^\/api\/v1/, ""), body: o };
|
|
25
|
+
}
|
|
26
|
+
const W = { level: "organization", id: null, name: null }, re = (n, s, e) => ({ level: n, id: s, name: e }), D = (n, s = {}) => {
|
|
27
|
+
const e = /^\d+$/.test(n), i = s.grants ?? [];
|
|
28
|
+
return {
|
|
29
|
+
ref: n,
|
|
30
|
+
id: e ? Number(n) : null,
|
|
31
|
+
key: e ? null : n,
|
|
32
|
+
name: `Role ${n}`,
|
|
33
|
+
description: "",
|
|
34
|
+
predefined: !e,
|
|
35
|
+
computed: !1,
|
|
36
|
+
attrs: [],
|
|
37
|
+
holders: 0,
|
|
38
|
+
permission_count: i.length,
|
|
39
|
+
...s,
|
|
40
|
+
grants: i
|
|
41
|
+
};
|
|
42
|
+
}, U = ({ grants: n, ...s }) => s, B = (n, s, e, i = null) => ({ id: n, role: U(s), scope: e, created_by: i, created_at: "2026-01-01T00:00:00Z" }), le = (n) => j(400, "authz.invalid", { params: { problems: n } }), H = (n, s) => {
|
|
43
|
+
const e = new Map(n.grants.map((o) => [o.permission, o.qualifier])), i = new Map(s.grants.map((o) => [o.permission, o.qualifier]));
|
|
44
|
+
return {
|
|
45
|
+
only_in_role: n.grants.filter((o) => !i.has(o.permission)),
|
|
46
|
+
only_in_other: s.grants.filter((o) => !e.has(o.permission)),
|
|
47
|
+
different: n.grants.filter((o) => i.has(o.permission) && i.get(o.permission) !== o.qualifier).map((o) => ({ permission: o.permission, role: o.qualifier, other: i.get(o.permission) }))
|
|
48
|
+
};
|
|
49
|
+
}, X = (n, s) => {
|
|
50
|
+
const e = /* @__PURE__ */ new Map();
|
|
51
|
+
for (const i of n)
|
|
52
|
+
for (const o of s.find((t) => t.ref === i.role.ref)?.grants ?? []) {
|
|
53
|
+
const t = e.get(o.permission) ?? { permission: o.permission, unavailable: !1, grants: [] };
|
|
54
|
+
t.grants.push({ qualifier: o.qualifier, scope: i.scope, attrs: i.role.attrs, binding_id: i.id, role_key: i.role.key ?? "", role_name: i.role.name }), e.set(o.permission, t);
|
|
55
|
+
}
|
|
56
|
+
return [...e.values()];
|
|
57
|
+
}, N = (n) => Math.max(0, ...n) + 1;
|
|
58
|
+
async function ce(n, s = {}) {
|
|
59
|
+
const e = {
|
|
60
|
+
calls: [],
|
|
61
|
+
roles: structuredClone(s.roles ?? []),
|
|
62
|
+
bindings: structuredClone(s.bindings ?? {}),
|
|
63
|
+
reads: []
|
|
64
|
+
}, i = s.places ?? { root_level: "organization", root: !0, places: [] };
|
|
65
|
+
let o = s.next?.role ?? N(e.roles.flatMap((l) => l.id === null ? [] : [l.id])), t = s.next?.binding ?? N(Object.values(e.bindings).flat().map((l) => l.id));
|
|
66
|
+
const f = (l) => e.roles.find((d) => d.ref === l);
|
|
67
|
+
return await n.route(/\/api\/v1\/iam\/(roles|bindable-roles|service-accounts\/bindable-roles|users\/\d+\/(access|scopes|bindings)|service-accounts\/\d+\/(access|scopes|bindings))/, async (l) => {
|
|
68
|
+
var _;
|
|
69
|
+
const { method: d, url: p, path: v, body: g } = S(l);
|
|
70
|
+
if (d !== "GET" ? e.calls.push({ method: d, path: v, body: g }) : e.reads.push(`${v}${p.search}`), await s.hook?.(l, p, d, g)) return;
|
|
71
|
+
if (v === "/iam/bindable-roles" || v === "/iam/service-accounts/bindable-roles") return l.fulfill(a(u({ roles: s.bindable ?? e.roles.filter((r) => r.predefined) })));
|
|
72
|
+
const m = /^\/iam\/roles(?:\/([^/]+)(?:\/(holders|compare|replace))?)?$/.exec(v);
|
|
73
|
+
if (m) {
|
|
74
|
+
const [, r, c] = m;
|
|
75
|
+
if (!r) {
|
|
76
|
+
if (d === "GET") return l.fulfill(a(u({ roles: e.roles.map(U) })));
|
|
77
|
+
const b = g, E = D(String(o++), { name: b.name, description: b.description, grants: b.grants, attrs: b.attrs, holders: 0 });
|
|
78
|
+
return e.roles.push(E), l.fulfill(a(u(E), 201));
|
|
79
|
+
}
|
|
80
|
+
const h = f(r);
|
|
81
|
+
if (!h) return l.fulfill(a({ success: !1, message: "not found", code: "access.role_not_found" }, 404));
|
|
82
|
+
if (c === "holders") return l.fulfill(a(u({ holders: s.holders?.[r] ?? [] })));
|
|
83
|
+
if (c === "compare") {
|
|
84
|
+
const b = f(p.searchParams.get("with") ?? "");
|
|
85
|
+
return b ? l.fulfill(a(u(H(h, b)))) : l.fulfill(a({ success: !1, message: "not found" }, 404));
|
|
86
|
+
}
|
|
87
|
+
if (c === "replace") return l.fulfill(a(u({ rebound: h.holders })));
|
|
88
|
+
if (d === "GET") return l.fulfill(a(u(h)));
|
|
89
|
+
if (d === "DELETE")
|
|
90
|
+
return e.roles = e.roles.filter((b) => b.ref !== r), l.fulfill(w());
|
|
91
|
+
const T = g;
|
|
92
|
+
return Object.assign(h, { name: T.name, description: T.description, grants: T.grants, attrs: T.attrs, permission_count: T.grants.length }), l.fulfill(a(u(h)));
|
|
93
|
+
}
|
|
94
|
+
const y = /^\/iam\/(users|service-accounts)\/(\d+)\/(access|scopes|bindings)(?:\/(\d+))?$/.exec(v);
|
|
95
|
+
if (y) {
|
|
96
|
+
const [, r, c, h, T] = y, b = `${r === "users" ? "user" : "service"}:${c}`, E = (_ = e.bindings)[b] ?? (_[b] = []);
|
|
97
|
+
if (h === "scopes") return l.fulfill(a(u(i)));
|
|
98
|
+
if (h === "access")
|
|
99
|
+
return l.fulfill(a(u({ subject: { kind: r === "users" ? "user" : "service", id: Number(c) }, bindings: E, effective: X(E, e.roles), can_manage: s.canManage ?? !0 })));
|
|
100
|
+
if (d === "DELETE")
|
|
101
|
+
return e.bindings[b] = E.filter((q) => q.id !== Number(T)), l.fulfill(w());
|
|
102
|
+
const k = g, I = k.level === "organization" ? W : { level: k.level, id: k.scope_id ?? null, name: i.places.find((q) => q.id === k.scope_id && q.level === k.level)?.name ?? null }, P = B(t++, f(k.role_ref) ?? D(k.role_ref), I);
|
|
103
|
+
return E.push(P), l.fulfill(a(u(P), 201));
|
|
104
|
+
}
|
|
105
|
+
return l.fallback();
|
|
106
|
+
}), e;
|
|
107
|
+
}
|
|
108
|
+
const oe = (n, s, e = {}) => ({
|
|
109
|
+
id: n,
|
|
110
|
+
category: e.category ?? "general",
|
|
111
|
+
title: s,
|
|
112
|
+
body: e.body ?? "",
|
|
113
|
+
link: e.link ?? "",
|
|
114
|
+
data: {},
|
|
115
|
+
read_at: e.read ? "2026-09-14T11:00:00Z" : null,
|
|
116
|
+
created_at: "2026-09-14T12:00:00Z"
|
|
117
|
+
}), C = (n, s = "default") => ({ enabled: n, source: s }), Y = (n, s, e = {}) => ({
|
|
118
|
+
id: n,
|
|
119
|
+
label: s,
|
|
120
|
+
platform: "windows",
|
|
121
|
+
endpoint_hash: `hash-${n}`,
|
|
122
|
+
active: !0,
|
|
123
|
+
created_at: "2026-09-10T09:00:00Z",
|
|
124
|
+
last_seen_at: "2026-09-13T18:00:00Z",
|
|
125
|
+
last_success_at: null,
|
|
126
|
+
...e
|
|
127
|
+
});
|
|
128
|
+
async function fe(n, s = {}) {
|
|
129
|
+
const e = {
|
|
130
|
+
calls: [],
|
|
131
|
+
items: s.items ?? [],
|
|
132
|
+
unread: 0,
|
|
133
|
+
categories: s.categories ?? [],
|
|
134
|
+
quiet: s.quiet ?? { enabled: !0, start: 1260, end: 420 },
|
|
135
|
+
devices: s.devices ?? [],
|
|
136
|
+
streams: 0
|
|
137
|
+
}, i = () => e.unread = s.unread ?? e.items.filter((t) => t.read_at === null).length;
|
|
138
|
+
i(), await n.route(/\/api\/v1\/notifications(\/|\?|$)/, (t) => o(t));
|
|
139
|
+
async function o(t) {
|
|
140
|
+
const { method: f, url: l, body: d } = S(t), p = l.pathname.replace("/api/v1/notifications", "") || "/";
|
|
141
|
+
f !== "GET" && e.calls.push({ method: f, path: p, body: d });
|
|
142
|
+
const v = () => a(u({ unread_count: e.unread })), g = () => u({ items: e.devices });
|
|
143
|
+
if (p === "/stream")
|
|
144
|
+
return e.streams++, t.fulfill(R(s.stream?.(e.streams, e) ?? [{ type: "unread", unread_count: e.unread }]));
|
|
145
|
+
if (p === "/" && f === "GET") {
|
|
146
|
+
if (s.failList?.()) return t.fulfill(a({ success: !1, message: "Server error" }, 500));
|
|
147
|
+
const r = Number(l.searchParams.get("before_id") ?? 0);
|
|
148
|
+
return t.fulfill(a(u({ items: e.items.filter((c) => !r || c.id < r), has_more: !1 })));
|
|
149
|
+
}
|
|
150
|
+
if (p === "/unread") return t.fulfill(v());
|
|
151
|
+
if (p === "/read") {
|
|
152
|
+
const r = d.up_to_id;
|
|
153
|
+
return e.items.forEach((c) => {
|
|
154
|
+
c.id <= r && c.read_at === null && (c.read_at = "2026-09-14T12:30:00Z");
|
|
155
|
+
}), i(), t.fulfill(v());
|
|
156
|
+
}
|
|
157
|
+
const m = p.match(/^\/(\d+)\/read$/);
|
|
158
|
+
if (m) {
|
|
159
|
+
const r = e.items.find((c) => c.id === Number(m[1]));
|
|
160
|
+
return r && (r.read_at = "2026-09-14T12:30:00Z"), i(), t.fulfill(v());
|
|
161
|
+
}
|
|
162
|
+
if (p === "/preferences")
|
|
163
|
+
return t.fulfill(a(u({
|
|
164
|
+
email_available: s.emailAvailable ?? !0,
|
|
165
|
+
push_available: s.pushAvailable ?? s.push?.enabled ?? !1,
|
|
166
|
+
categories: e.categories,
|
|
167
|
+
quiet_hours: e.quiet,
|
|
168
|
+
time_zone: s.timeZone ?? "UTC"
|
|
169
|
+
})));
|
|
170
|
+
const y = p.match(/^\/preferences\/([^/]+?)(\/push)?$/);
|
|
171
|
+
if (y && f === "PUT") {
|
|
172
|
+
const r = e.categories.find((c) => c.category === decodeURIComponent(y[1]));
|
|
173
|
+
return y[2] ? r.push = C(d.push, "person") : r.email = C(d.email, "person"), t.fulfill(a(u(r)));
|
|
174
|
+
}
|
|
175
|
+
if (p === "/quiet-hours")
|
|
176
|
+
return e.quiet = d, t.fulfill(a(u(e.quiet)));
|
|
177
|
+
if (p === "/push") return t.fulfill(a(u(s.push ?? { enabled: !1, public_key: "" })));
|
|
178
|
+
if (p === "/devices" && f === "GET") return t.fulfill(a(g()));
|
|
179
|
+
if (p === "/devices" && f === "POST") {
|
|
180
|
+
const r = s.registers ?? Y(1, "Mac · Chrome", { platform: "macos" });
|
|
181
|
+
return e.devices = [r, ...e.devices.filter((c) => c.id !== r.id)], t.fulfill(a(u(r)));
|
|
182
|
+
}
|
|
183
|
+
if (p === "/devices/unsubscribe") {
|
|
184
|
+
const r = s.registers;
|
|
185
|
+
return e.devices = e.devices.filter((c) => c.endpoint_hash !== r?.endpoint_hash), t.fulfill(a(g()));
|
|
186
|
+
}
|
|
187
|
+
const _ = p.match(/^\/devices\/(\d+)(\/test)?$/);
|
|
188
|
+
if (_) {
|
|
189
|
+
const r = Number(_[1]);
|
|
190
|
+
return f === "PATCH" && (e.devices = e.devices.map((c) => c.id === r ? { ...c, label: d.label } : c)), f === "DELETE" && (e.devices = e.devices.filter((c) => c.id !== r)), t.fulfill(a(f === "PATCH" ? u(e.devices.find((c) => c.id === r)) : g()));
|
|
191
|
+
}
|
|
192
|
+
return t.fulfill(a(u({})));
|
|
193
|
+
}
|
|
194
|
+
return e;
|
|
195
|
+
}
|
|
196
|
+
const K = "2099-01-01T09:30:00.000Z", Q = "2020-01-01T00:00:00.000Z", $ = "new@example.test", A = (n) => new Date(Date.now() - n * 6e4).toISOString(), V = (n = {}) => ({
|
|
197
|
+
id: 1,
|
|
198
|
+
login: "admin",
|
|
199
|
+
name: "Test Admin",
|
|
200
|
+
email: "admin@test.com",
|
|
201
|
+
phone: "",
|
|
202
|
+
locale: "en",
|
|
203
|
+
created_at: "2025-02-03T10:12:00Z",
|
|
204
|
+
pending_email: null,
|
|
205
|
+
...n
|
|
206
|
+
}), ee = (n = {}) => ({ email: $, expires_at: K, resend_available_at: Q, attempts_left: 3, ...n }), se = (n, s, e = {}) => ({
|
|
207
|
+
id: n,
|
|
208
|
+
device: s,
|
|
209
|
+
ip: "10.20.4.12",
|
|
210
|
+
opened_by: null,
|
|
211
|
+
current: !1,
|
|
212
|
+
last_used_at: A(2),
|
|
213
|
+
expires_at: new Date(Date.now() + 6 * 36e5).toISOString(),
|
|
214
|
+
created_at: A(300),
|
|
215
|
+
...e
|
|
216
|
+
}), ne = (n, s, e = {}) => ({
|
|
217
|
+
id: n,
|
|
218
|
+
event: s,
|
|
219
|
+
ip: "93.142.8.201",
|
|
220
|
+
device: "Chrome · Windows",
|
|
221
|
+
actor: null,
|
|
222
|
+
created_at: A(n * 20),
|
|
223
|
+
...e
|
|
224
|
+
});
|
|
225
|
+
async function ue(n, s = {}) {
|
|
226
|
+
const e = [], i = s.answers ?? {}, o = () => ee({ email: s.newEmail ?? $ }), t = { profile: s.profile ?? V(), sessions: s.sessions ?? [se(11, "Chrome · Linux", { current: !0 })], signins: s.signins ?? [ne(1, "signed_in")] };
|
|
227
|
+
return await n.route("**/api/v1/iam/profile**", (f) => {
|
|
228
|
+
const { method: l, path: d, body: p } = S(f);
|
|
229
|
+
if (l !== "GET" && e.push({ method: l, path: d, body: p ?? null }), d === "/iam/profile" && l === "GET") return f.fulfill(a(u(t.profile)));
|
|
230
|
+
if (d === "/iam/profile/email" && l === "POST")
|
|
231
|
+
return i.request ? f.fulfill(i.request()) : (t.profile = { ...t.profile, pending_email: o() }, f.fulfill(a(u(o()))));
|
|
232
|
+
if (d === "/iam/profile/email/resend") return f.fulfill(i.resend ? i.resend() : a(u(o())));
|
|
233
|
+
if (d === "/iam/profile/email/confirm")
|
|
234
|
+
return i.confirm ? f.fulfill(i.confirm()) : (t.profile = { ...t.profile, email: s.newEmail ?? $, pending_email: null }, f.fulfill(a(u(t.profile))));
|
|
235
|
+
if (d === "/iam/profile/email" && l === "DELETE")
|
|
236
|
+
return i.cancel ? f.fulfill(i.cancel()) : (t.profile = { ...t.profile, pending_email: null }, f.fulfill(w()));
|
|
237
|
+
if (d === "/iam/profile/password") return f.fulfill(i.password ? i.password() : w());
|
|
238
|
+
if (d === "/iam/profile/sessions" && l === "GET") return f.fulfill(a(u({ sessions: t.sessions })));
|
|
239
|
+
const v = /^\/iam\/profile\/sessions\/(\d+)$/.exec(d);
|
|
240
|
+
return v && l === "DELETE" ? (t.sessions = t.sessions.filter((g) => g.id !== Number(v[1])), f.fulfill(w())) : d === "/iam/profile/signins" ? f.fulfill(a(O(t.signins))) : f.fallback();
|
|
241
|
+
}), { calls: e, state: t };
|
|
242
|
+
}
|
|
243
|
+
const x = (n) => new Date(Date.now() - n * 6e4).toISOString(), G = (n, s, e = {}) => ({
|
|
244
|
+
id: n,
|
|
245
|
+
login: s.split(" ")[0].toLowerCase(),
|
|
246
|
+
name: s,
|
|
247
|
+
email: `${s.toLowerCase().replace(/[^a-z]+/g, ".")}@example.test`,
|
|
248
|
+
phone: "",
|
|
249
|
+
locale: "en",
|
|
250
|
+
active: !0,
|
|
251
|
+
status: "active",
|
|
252
|
+
last_sign_in: null,
|
|
253
|
+
locked_until: null,
|
|
254
|
+
created_at: "2025-02-03T10:12:00Z",
|
|
255
|
+
...e
|
|
256
|
+
}), de = (n, s, e = {}) => ({
|
|
257
|
+
id: n,
|
|
258
|
+
device: s,
|
|
259
|
+
ip: "10.20.4.12",
|
|
260
|
+
last_used_at: x(2),
|
|
261
|
+
expires_at: new Date(Date.now() + 6 * 36e5).toISOString(),
|
|
262
|
+
created_at: x(300),
|
|
263
|
+
opened_by: null,
|
|
264
|
+
current: !1,
|
|
265
|
+
...e
|
|
266
|
+
}), me = (n, s, e = {}) => ({
|
|
267
|
+
id: n,
|
|
268
|
+
event: s,
|
|
269
|
+
ip: "93.142.8.201",
|
|
270
|
+
device: "Chrome · Windows",
|
|
271
|
+
actor: null,
|
|
272
|
+
created_at: x(n * 20),
|
|
273
|
+
...e
|
|
274
|
+
}), pe = (n, s, e = {}) => ({
|
|
275
|
+
id: n,
|
|
276
|
+
action: s,
|
|
277
|
+
fields: [],
|
|
278
|
+
before: {},
|
|
279
|
+
after: {},
|
|
280
|
+
actor: { id: 4, name: "Test Manager" },
|
|
281
|
+
created_at: x(n * 90),
|
|
282
|
+
...e
|
|
283
|
+
}), ge = (n, s, e, i, o = {}) => ({
|
|
284
|
+
id: n,
|
|
285
|
+
area: "general",
|
|
286
|
+
record_type: s,
|
|
287
|
+
record_id: e,
|
|
288
|
+
action: i,
|
|
289
|
+
signed_in_as: null,
|
|
290
|
+
created_at: x(n * 40),
|
|
291
|
+
...o
|
|
292
|
+
}), ve = (n, s, e = {}) => ({
|
|
293
|
+
id: n,
|
|
294
|
+
name: s,
|
|
295
|
+
description: "",
|
|
296
|
+
tenant: null,
|
|
297
|
+
owner: { id: 1, name: "Test Admin" },
|
|
298
|
+
created_by: { id: 1, name: "Test Admin" },
|
|
299
|
+
created_at: "2026-09-12T10:00:00Z",
|
|
300
|
+
active_keys: 1,
|
|
301
|
+
last_used_at: x(3),
|
|
302
|
+
deactivated_at: null,
|
|
303
|
+
...e
|
|
304
|
+
}), ie = (n = {}) => ({
|
|
305
|
+
people: [],
|
|
306
|
+
sessions: [],
|
|
307
|
+
signins: [],
|
|
308
|
+
changes: [],
|
|
309
|
+
activity: [],
|
|
310
|
+
serviceAccounts: [],
|
|
311
|
+
keys: {},
|
|
312
|
+
notManageable: !1,
|
|
313
|
+
...n
|
|
314
|
+
}), L = (n) => a({ success: !1, error: "not found", message: "not found", ...n ? { code: n } : {} }, 404);
|
|
315
|
+
async function he(n, s = {}) {
|
|
316
|
+
const e = s.writes ?? [], i = s.state ?? ie(), o = ({ method: t, path: f, url: l, body: d }) => {
|
|
317
|
+
if (f === "/iam/users" && t === "GET") {
|
|
318
|
+
const g = l.searchParams.get("view") ?? "active", m = (l.searchParams.get("search") ?? "").toLowerCase(), y = i.people.filter((r) => (!m || r.name.toLowerCase().includes(m)) && (s.filter?.(r, l.searchParams) ?? !0)), _ = (r) => y.filter((c) => r === "all" || c.status === r).length;
|
|
319
|
+
return a(O(y.filter((r) => g === "all" || r.status === g), ["active", "locked", "inactive", "all"].map((r) => ({ key: r, count: _(r) }))));
|
|
320
|
+
}
|
|
321
|
+
if (f === "/iam/users" && t === "POST") {
|
|
322
|
+
const g = d, m = G(Math.max(0, ...i.people.map((y) => y.id)) + 1, g.name, g);
|
|
323
|
+
return i.people.push(m), a(u(m));
|
|
324
|
+
}
|
|
325
|
+
const p = /^\/iam\/users\/(\d+)(?:\/(.*))?$/.exec(f);
|
|
326
|
+
if (p) {
|
|
327
|
+
const g = Number(p[1]), m = p[2] ?? "", _ = i.people.find((c) => c.id === g) ?? G(g, "Unknown Person");
|
|
328
|
+
if (m === "") {
|
|
329
|
+
if (t === "PUT") {
|
|
330
|
+
const c = d, h = c.ext === void 0 ? _.ext : { ..._.ext, ...c.ext };
|
|
331
|
+
return a(u({ ..._, ...c, ...h === void 0 ? {} : { ext: h } }));
|
|
332
|
+
}
|
|
333
|
+
return t === "DELETE" ? w() : a(u(_));
|
|
334
|
+
}
|
|
335
|
+
if (m === "sessions" && t === "GET") return a(u({ sessions: i.sessions }));
|
|
336
|
+
if (m === "sessions" && t === "DELETE")
|
|
337
|
+
return i.sessions = [], w();
|
|
338
|
+
const r = /^sessions\/(\d+)$/.exec(m);
|
|
339
|
+
if (r && t === "DELETE")
|
|
340
|
+
return i.sessions = i.sessions.filter((c) => c.id !== Number(r[1])), w();
|
|
341
|
+
if (m === "signins") {
|
|
342
|
+
const h = l.searchParams.get("view") === "failed" ? i.signins.filter((T) => ["wrong_password", "locked_out", "refused_inactive"].includes(T.event)) : i.signins;
|
|
343
|
+
return a(O(h, [{ key: "all", count: i.signins.length }, { key: "failed", count: i.signins.filter((T) => ["wrong_password", "locked_out", "refused_inactive"].includes(T.event)).length }]));
|
|
344
|
+
}
|
|
345
|
+
if (m === "changes") return a(O(i.changes, [{ key: "all", count: i.changes.length }]));
|
|
346
|
+
if (m === "activity") {
|
|
347
|
+
const c = l.searchParams.get("area");
|
|
348
|
+
return a(O(c ? i.activity.filter((h) => h.area === c) : i.activity, [{ key: "all", count: i.activity.length }]));
|
|
349
|
+
}
|
|
350
|
+
return m === "deactivate" || m === "activate" ? a(u({})) : m === "password" ? w() : m === "unlock" ? (_.locked_until = null, _.status = "active", a(u({ unlocked: !0 }))) : void 0;
|
|
351
|
+
}
|
|
352
|
+
if (f === "/iam/service-accounts") return a(u({ service_accounts: i.serviceAccounts }));
|
|
353
|
+
const v = /^\/iam\/service-accounts\/(\d+)(?:\/(keys))?$/.exec(f);
|
|
354
|
+
if (v && t === "GET") {
|
|
355
|
+
const g = i.serviceAccounts.find((m) => m.id === Number(v[1]));
|
|
356
|
+
return g ? a(u(v[2] ? { keys: i.keys[g.id] ?? [] } : g)) : L();
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
return await n.route(/\/api\/v1\/iam\/(users|service-accounts)(\/|\?|$)/, (t) => {
|
|
360
|
+
const { method: f, url: l, path: d, body: p } = S(t), v = { method: f, path: d, url: l, body: p }, m = (f !== "GET" && i.notManageable && /^\/iam\/users\/\d+/.test(d) ? L("identity.account.not_found") : void 0) ?? s.hook?.(v, i) ?? o(v);
|
|
361
|
+
return m ? (f !== "GET" && e.push({ method: f, path: d, body: p ?? null }), t.fulfill(m)) : t.fallback();
|
|
362
|
+
}), { writes: e, state: i };
|
|
363
|
+
}
|
|
364
|
+
export {
|
|
365
|
+
$ as NEW_EMAIL,
|
|
366
|
+
W as ORGANIZATION,
|
|
367
|
+
M as accessOf,
|
|
368
|
+
ge as activity,
|
|
369
|
+
x as ago,
|
|
370
|
+
J as authBody,
|
|
371
|
+
F as authResponse,
|
|
372
|
+
B as bindingOf,
|
|
373
|
+
pe as change,
|
|
374
|
+
O as datatable,
|
|
375
|
+
Z as defaultUser,
|
|
376
|
+
Y as device,
|
|
377
|
+
ie as freshState,
|
|
378
|
+
le as invalid,
|
|
379
|
+
a as json,
|
|
380
|
+
ae as mockAuth,
|
|
381
|
+
w as noContent,
|
|
382
|
+
oe as notification,
|
|
383
|
+
u as ok,
|
|
384
|
+
se as ownSession,
|
|
385
|
+
ne as ownSignin,
|
|
386
|
+
te as pageOk,
|
|
387
|
+
ee as pendingChange,
|
|
388
|
+
G as person,
|
|
389
|
+
V as profile,
|
|
390
|
+
j as refusal,
|
|
391
|
+
D as role,
|
|
392
|
+
re as scopeOf,
|
|
393
|
+
ce as serveAccess,
|
|
394
|
+
fe as serveNotifications,
|
|
395
|
+
ue as serveProfile,
|
|
396
|
+
he as serveUsers,
|
|
397
|
+
ve as serviceAccount,
|
|
398
|
+
de as session,
|
|
399
|
+
C as setting,
|
|
400
|
+
me as signin,
|
|
401
|
+
R as sse,
|
|
402
|
+
U as summary
|
|
403
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test";
|
|
2
|
+
import type { CategorySettings, Device, Item, PushConfig, QuietHours, Setting, Source, StreamEvent } from "../modules/notification/entities";
|
|
3
|
+
import { type Call } from "./request";
|
|
4
|
+
/**
|
|
5
|
+
* go-core's notification wire as it serves it under `/api/v1/notifications`, for the specs that open the bell, the settings or the
|
|
6
|
+
* failed-events page: the inbox, the live stream, the preferences, push and the devices. A fake with a little state (items, unread
|
|
7
|
+
* count, preferences, devices), so a spec reads what it did through `calls`. The page needs `mockAuth` too.
|
|
8
|
+
*/
|
|
9
|
+
export type { CategorySettings, Device, Item, QuietHours, Setting };
|
|
10
|
+
export type { Call } from "./request";
|
|
11
|
+
export declare const notification: (id: number, title: string, options?: {
|
|
12
|
+
read?: boolean;
|
|
13
|
+
category?: string;
|
|
14
|
+
link?: string;
|
|
15
|
+
body?: string;
|
|
16
|
+
}) => Item;
|
|
17
|
+
export declare const setting: (enabled: boolean, source?: Source) => Setting;
|
|
18
|
+
export declare const device: (id: number, label: string, options?: Partial<Device>) => Device;
|
|
19
|
+
export type WireOptions = {
|
|
20
|
+
items?: Item[];
|
|
21
|
+
/** The unread count the server says; default the number of unread items. */
|
|
22
|
+
unread?: number;
|
|
23
|
+
categories?: CategorySettings[];
|
|
24
|
+
/** Minutes after midnight: 1260 is 21:00. */
|
|
25
|
+
quiet?: QuietHours;
|
|
26
|
+
timeZone?: string;
|
|
27
|
+
emailAvailable?: boolean;
|
|
28
|
+
pushAvailable?: boolean;
|
|
29
|
+
push?: PushConfig;
|
|
30
|
+
devices?: Device[];
|
|
31
|
+
/** The device a registration creates (the browser's own). */
|
|
32
|
+
registers?: Device;
|
|
33
|
+
/** What each connection of the live stream sends, by its number from 1; default one `unread` snapshot. */
|
|
34
|
+
stream?: (connection: number, state: Wire) => StreamEvent[];
|
|
35
|
+
/** Fails the inbox list while true. */
|
|
36
|
+
failList?: () => boolean;
|
|
37
|
+
};
|
|
38
|
+
export type Wire = {
|
|
39
|
+
calls: Call[];
|
|
40
|
+
items: Item[];
|
|
41
|
+
unread: number;
|
|
42
|
+
categories: CategorySettings[];
|
|
43
|
+
quiet: QuietHours;
|
|
44
|
+
devices: Device[];
|
|
45
|
+
streams: number;
|
|
46
|
+
};
|
|
47
|
+
/** Answers every `/api/v1/notifications/**` route like go-core. */
|
|
48
|
+
export declare function serveNotifications(page: Page, options?: WireOptions): Promise<Wire>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test";
|
|
2
|
+
import type { PendingEmail, ProfileResponse, SessionItem, SignInRow } from "../modules/identity/entities";
|
|
3
|
+
import { type Answer } from "./envelope";
|
|
4
|
+
import { type Call } from "./request";
|
|
5
|
+
/**
|
|
6
|
+
* "My profile" as the identity module serves it (`/api/v1/iam/profile…`, go-core `ProfileResponse`): the record, the e-mail change
|
|
7
|
+
* with its confirmation, the password, the person's own sessions and sign-ins. Every write is reported with its body; an endpoint
|
|
8
|
+
* answers as `answers` says (success when absent). Routes your application adds under `iam/profile` are yours: register them
|
|
9
|
+
* after this, they run first, and this answers the rest.
|
|
10
|
+
*/
|
|
11
|
+
export type ProfileCall = Call;
|
|
12
|
+
/** The address a confirmed e-mail change ends with (default; `newEmail` changes it). */
|
|
13
|
+
export declare const NEW_EMAIL = "new@example.test";
|
|
14
|
+
export declare const profile: (overrides?: Partial<ProfileResponse>) => ProfileResponse;
|
|
15
|
+
export declare const pendingChange: (overrides?: Partial<PendingEmail>) => PendingEmail;
|
|
16
|
+
export declare const ownSession: (id: number, device: string, overrides?: Partial<SessionItem>) => SessionItem;
|
|
17
|
+
export declare const ownSignin: (id: number, event: SignInRow["event"], overrides?: Partial<SignInRow>) => SignInRow;
|
|
18
|
+
export type ProfileAnswers = Partial<Record<"request" | "confirm" | "resend" | "cancel" | "password", () => Answer>>;
|
|
19
|
+
export type ProfileOptions = {
|
|
20
|
+
profile?: ProfileResponse;
|
|
21
|
+
answers?: ProfileAnswers;
|
|
22
|
+
sessions?: SessionItem[];
|
|
23
|
+
signins?: SignInRow[];
|
|
24
|
+
/** The address a request is for and a confirmation lands on (default `NEW_EMAIL`). */
|
|
25
|
+
newEmail?: string;
|
|
26
|
+
};
|
|
27
|
+
/** Registers the profile routes (after `mockAuth`). */
|
|
28
|
+
export declare function serveProfile(page: Page, options?: ProfileOptions): Promise<{
|
|
29
|
+
calls: Call[];
|
|
30
|
+
state: {
|
|
31
|
+
profile: ProfileResponse;
|
|
32
|
+
sessions: SessionItem[];
|
|
33
|
+
signins: SignInRow[];
|
|
34
|
+
};
|
|
35
|
+
}>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Route } from "@playwright/test";
|
|
2
|
+
/** What a handler needs of a request: its method, the path below `/api/v1`, the query and the JSON body (undefined for a read or an empty body). */
|
|
3
|
+
export declare function readRequest(route: Route): {
|
|
4
|
+
method: string;
|
|
5
|
+
url: URL;
|
|
6
|
+
path: string;
|
|
7
|
+
body: unknown;
|
|
8
|
+
};
|
|
9
|
+
/** One write a mock saw: a spec reads what the page sent through these. */
|
|
10
|
+
export type Call = {
|
|
11
|
+
method: string;
|
|
12
|
+
path: string;
|
|
13
|
+
body: unknown;
|
|
14
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test";
|
|
2
|
+
import type { AccessQualifier } from "../platform";
|
|
3
|
+
import type { MyAccess, Node, SessionResponse, User } from "../modules/identity/entities";
|
|
4
|
+
/** The signed-in person a spec starts with: only what go-core's identity returns. An app adds its own fields through `mockAuth`'s `user`. */
|
|
5
|
+
export declare const defaultUser: User;
|
|
6
|
+
export type MockAuthOptions = {
|
|
7
|
+
/** The signed-in user's navigation (default none). */
|
|
8
|
+
navigation?: Node[];
|
|
9
|
+
/** Fields merged over `defaultUser`: a different person, or the fields your application adds to the user. */
|
|
10
|
+
user?: Partial<User> & Record<string, unknown>;
|
|
11
|
+
/** The qualifier each permission is held at (default `all`), e.g. `{ "crm.lead:view": "own_location" }`. */
|
|
12
|
+
access?: Record<string, AccessQualifier>;
|
|
13
|
+
/** Where every permission is held (default the organization, which is the root: no id). */
|
|
14
|
+
scope?: HeldAt;
|
|
15
|
+
};
|
|
16
|
+
/** Where a permission is held: the organization, or a place of a level (`{ level: "dealer", id: 1 }`). */
|
|
17
|
+
export type HeldAt = {
|
|
18
|
+
level: string;
|
|
19
|
+
id?: number;
|
|
20
|
+
};
|
|
21
|
+
/** Fields of the session payload to set or replace (an `impersonator`, another `expires_at`), and any the application adds. */
|
|
22
|
+
export type SessionExtra = Partial<Omit<SessionResponse, "access">> & {
|
|
23
|
+
access?: MyAccess;
|
|
24
|
+
} & Record<string, unknown>;
|
|
25
|
+
/** The `/auth/me` `access` payload for the given permissions, every one at `scope` with its qualifier. The organization has no id: it is go-core's root scope, and `root` says a binding is there. */
|
|
26
|
+
export declare function accessOf(policies: string[], qualifiers?: Record<string, AccessQualifier>, scope?: HeldAt): MyAccess;
|
|
27
|
+
/** The session payload of go-core's identity (`/auth/me`, `/auth/refresh`, `/auth/login-as`), without its envelope. */
|
|
28
|
+
export declare function authResponse(policies?: string[], extra?: SessionExtra): {
|
|
29
|
+
navigation: Node[];
|
|
30
|
+
user: unknown;
|
|
31
|
+
impersonator?: (import("../identity").Impersonator | null) | undefined;
|
|
32
|
+
expires_at: string;
|
|
33
|
+
access: MyAccess;
|
|
34
|
+
};
|
|
35
|
+
/** The same, as the server sends it: inside go-core's `{ success, data }` envelope. */
|
|
36
|
+
export declare const authBody: (policies?: string[], extra?: SessionExtra) => {
|
|
37
|
+
meta?: {} | null | undefined;
|
|
38
|
+
success: true;
|
|
39
|
+
data: {
|
|
40
|
+
navigation: Node[];
|
|
41
|
+
user: unknown;
|
|
42
|
+
impersonator?: (import("../identity").Impersonator | null) | undefined;
|
|
43
|
+
expires_at: string;
|
|
44
|
+
access: MyAccess;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
/** Signs the page in: answers go-core identity's `GET /api/v1/auth/me` with a session that holds `policies`. */
|
|
48
|
+
export declare function mockAuth(page: Page, policies?: string[], options?: MockAuthOptions): Promise<void>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test";
|
|
2
|
+
import type { ActivityRow, ChangeRow, SessionItem, SignInRow, UserDetail } from "../modules/identity/entities";
|
|
3
|
+
import type { ApiKey, ServiceAccount } from "../modules/serviceaccount/entities";
|
|
4
|
+
import { type Answer } from "./envelope";
|
|
5
|
+
import { type Call } from "./request";
|
|
6
|
+
/**
|
|
7
|
+
* The people of the identity module as it serves them under `/api/v1/iam/users` (list with its views, one person's record, sessions,
|
|
8
|
+
* sign-ins, changes, activity, unlock, activate) and the service accounts of `/api/v1/iam/service-accounts` (list, record, keys).
|
|
9
|
+
* Every write is reported with its body. Only these paths are answered: the rest of the application's API, the access routes
|
|
10
|
+
* (`serveAccess`) and what your application adds to a person (`ext`) are yours. The rules themselves are the server's tests'.
|
|
11
|
+
*/
|
|
12
|
+
export type Write = Call;
|
|
13
|
+
export declare const ago: (minutes: number) => string;
|
|
14
|
+
/** A person as go-core's list row and record carry them (`UserRow` / `UserDetail`; the same shape). */
|
|
15
|
+
export declare const person: (id: number, name: string, overrides?: Partial<UserDetail>) => UserDetail;
|
|
16
|
+
/** A live session of a person (`SessionItem`). */
|
|
17
|
+
export declare const session: (id: number, device: string, overrides?: Partial<SessionItem>) => SessionItem;
|
|
18
|
+
/** A row of the sign-in history (`SignInRow`). */
|
|
19
|
+
export declare const signin: (id: number, event: SignInRow["event"], overrides?: Partial<SignInRow>) => SignInRow;
|
|
20
|
+
/** A change to the person (`ChangeRow`). */
|
|
21
|
+
export declare const change: (id: number, action: ChangeRow["action"], overrides?: Partial<ChangeRow>) => ChangeRow;
|
|
22
|
+
/** What a person did (`ActivityRow`). */
|
|
23
|
+
export declare const activity: (id: number, recordType: string, recordId: number, action: ActivityRow["action"], overrides?: Partial<ActivityRow>) => ActivityRow;
|
|
24
|
+
export declare const serviceAccount: (id: number, name: string, overrides?: Partial<ServiceAccount>) => ServiceAccount;
|
|
25
|
+
export type UsersState = {
|
|
26
|
+
people: UserDetail[];
|
|
27
|
+
sessions: SessionItem[];
|
|
28
|
+
signins: SignInRow[];
|
|
29
|
+
changes: ChangeRow[];
|
|
30
|
+
activity: ActivityRow[];
|
|
31
|
+
serviceAccounts: ServiceAccount[];
|
|
32
|
+
/** The keys of each service account, by its id. */
|
|
33
|
+
keys: Record<number, ApiKey[]>;
|
|
34
|
+
/** Whether the person's record answers `404` on manage actions (seen but not managed). */
|
|
35
|
+
notManageable: boolean;
|
|
36
|
+
};
|
|
37
|
+
export declare const freshState: (overrides?: Partial<UsersState>) => UsersState;
|
|
38
|
+
export type UsersRequest = {
|
|
39
|
+
method: string;
|
|
40
|
+
path: string;
|
|
41
|
+
url: URL;
|
|
42
|
+
body: unknown;
|
|
43
|
+
};
|
|
44
|
+
export type UsersOptions = {
|
|
45
|
+
state?: UsersState;
|
|
46
|
+
/** Where the writes are reported (default a new array, returned). */
|
|
47
|
+
writes?: Write[];
|
|
48
|
+
/** Whether a person is in the list for these query parameters, besides the view and the search (your `ext` filters). */
|
|
49
|
+
filter?: (person: UserDetail, params: URLSearchParams) => boolean;
|
|
50
|
+
/** Answers first (after `notManageable`): an answer is served (and a write reported), undefined leaves the request to the routes below. */
|
|
51
|
+
hook?: (request: UsersRequest, state: UsersState) => Answer | undefined;
|
|
52
|
+
};
|
|
53
|
+
/** Serves the users module; every write (with its JSON body) is reported, and the state can be changed by a spec. */
|
|
54
|
+
export declare function serveUsers(page: Page, options?: UsersOptions): Promise<{
|
|
55
|
+
writes: Call[];
|
|
56
|
+
state: UsersState;
|
|
57
|
+
}>;
|