@haven-chat-org/core 1.0.0
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/LICENSE +661 -0
- package/README.md +71 -0
- package/dist/crypto/backup.d.ts +87 -0
- package/dist/crypto/backup.js +62 -0
- package/dist/crypto/backup.js.map +1 -0
- package/dist/crypto/double-ratchet.d.ts +104 -0
- package/dist/crypto/double-ratchet.js +274 -0
- package/dist/crypto/double-ratchet.js.map +1 -0
- package/dist/crypto/file.d.ts +14 -0
- package/dist/crypto/file.js +20 -0
- package/dist/crypto/file.js.map +1 -0
- package/dist/crypto/index.d.ts +9 -0
- package/dist/crypto/index.js +10 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/crypto/keys.d.ts +61 -0
- package/dist/crypto/keys.js +79 -0
- package/dist/crypto/keys.js.map +1 -0
- package/dist/crypto/passphrase.d.ts +10 -0
- package/dist/crypto/passphrase.js +142 -0
- package/dist/crypto/passphrase.js.map +1 -0
- package/dist/crypto/profile.d.ts +31 -0
- package/dist/crypto/profile.js +73 -0
- package/dist/crypto/profile.js.map +1 -0
- package/dist/crypto/sender-keys.d.ts +76 -0
- package/dist/crypto/sender-keys.js +170 -0
- package/dist/crypto/sender-keys.js.map +1 -0
- package/dist/crypto/sender-keys.test.d.ts +1 -0
- package/dist/crypto/sender-keys.test.js +272 -0
- package/dist/crypto/sender-keys.test.js.map +1 -0
- package/dist/crypto/utils.d.ts +41 -0
- package/dist/crypto/utils.js +102 -0
- package/dist/crypto/utils.js.map +1 -0
- package/dist/crypto/x3dh.d.ts +45 -0
- package/dist/crypto/x3dh.js +106 -0
- package/dist/crypto/x3dh.js.map +1 -0
- package/dist/export/__tests__/archive.test.d.ts +1 -0
- package/dist/export/__tests__/archive.test.js +276 -0
- package/dist/export/__tests__/archive.test.js.map +1 -0
- package/dist/export/archive.d.ts +38 -0
- package/dist/export/archive.js +107 -0
- package/dist/export/archive.js.map +1 -0
- package/dist/export/index.d.ts +4 -0
- package/dist/export/index.js +4 -0
- package/dist/export/index.js.map +1 -0
- package/dist/export/reader.d.ts +27 -0
- package/dist/export/reader.js +101 -0
- package/dist/export/reader.js.map +1 -0
- package/dist/export/signing.d.ts +15 -0
- package/dist/export/signing.js +44 -0
- package/dist/export/signing.js.map +1 -0
- package/dist/export/types.d.ts +128 -0
- package/dist/export/types.js +3 -0
- package/dist/export/types.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/net/api.d.ts +200 -0
- package/dist/net/api.js +715 -0
- package/dist/net/api.js.map +1 -0
- package/dist/net/api.test.d.ts +1 -0
- package/dist/net/api.test.js +884 -0
- package/dist/net/api.test.js.map +1 -0
- package/dist/net/index.d.ts +2 -0
- package/dist/net/index.js +3 -0
- package/dist/net/index.js.map +1 -0
- package/dist/net/ws.d.ts +71 -0
- package/dist/net/ws.js +257 -0
- package/dist/net/ws.js.map +1 -0
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.js +2 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/memory.d.ts +24 -0
- package/dist/store/memory.js +50 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/types.d.ts +23 -0
- package/dist/store/types.js +2 -0
- package/dist/store/types.js.map +1 -0
- package/dist/types.d.ts +850 -0
- package/dist/types.js +35 -0
- package/dist/types.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
// Mock libsodium so tests don't need the native WASM module.
|
|
3
|
+
// solvePoW() calls initSodium + getSodium().crypto_hash_sha256.
|
|
4
|
+
// Returning all-zero bytes satisfies any difficulty check.
|
|
5
|
+
vi.mock("../crypto/utils.js", () => ({
|
|
6
|
+
initSodium: vi.fn(),
|
|
7
|
+
getSodium: vi.fn(() => ({
|
|
8
|
+
crypto_hash_sha256: () => new Uint8Array(32),
|
|
9
|
+
})),
|
|
10
|
+
}));
|
|
11
|
+
import { HavenApi, HavenApiError } from "./api.js";
|
|
12
|
+
// ── Mock fetch ──────────────────────────────────────────
|
|
13
|
+
function mockResponse(body, status = 200) {
|
|
14
|
+
return {
|
|
15
|
+
ok: status >= 200 && status < 300,
|
|
16
|
+
status,
|
|
17
|
+
statusText: status === 200 ? "OK" : "Error",
|
|
18
|
+
text: () => Promise.resolve(body != null ? JSON.stringify(body) : ""),
|
|
19
|
+
json: () => Promise.resolve(body),
|
|
20
|
+
headers: new Headers(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
let fetchMock;
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
fetchMock = vi.fn();
|
|
26
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.restoreAllMocks();
|
|
30
|
+
});
|
|
31
|
+
// ── Token management ────────────────────────────────────
|
|
32
|
+
describe("token management", () => {
|
|
33
|
+
it("starts with no token", () => {
|
|
34
|
+
const api = new HavenApi({ baseUrl: "http://localhost" });
|
|
35
|
+
expect(api.currentAccessToken).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
it("setTokens stores tokens", () => {
|
|
38
|
+
const api = new HavenApi({ baseUrl: "http://localhost" });
|
|
39
|
+
api.setTokens("access-123", "refresh-456");
|
|
40
|
+
expect(api.currentAccessToken).toBe("access-123");
|
|
41
|
+
});
|
|
42
|
+
it("clearTokens removes tokens", () => {
|
|
43
|
+
const api = new HavenApi({ baseUrl: "http://localhost" });
|
|
44
|
+
api.setTokens("access-123", "refresh-456");
|
|
45
|
+
api.clearTokens();
|
|
46
|
+
expect(api.currentAccessToken).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
// ── Auth endpoints ──────────────────────────────────────
|
|
50
|
+
describe("auth", () => {
|
|
51
|
+
it("login sends POST with correct body and sets tokens", async () => {
|
|
52
|
+
const authResponse = {
|
|
53
|
+
access_token: "at-1",
|
|
54
|
+
refresh_token: "rt-1",
|
|
55
|
+
user: { id: "uuid-1", username: "alice" },
|
|
56
|
+
};
|
|
57
|
+
fetchMock.mockResolvedValueOnce(mockResponse(authResponse));
|
|
58
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
59
|
+
const result = await api.login({ username: "alice", password: "secret" });
|
|
60
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
61
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
62
|
+
expect(url).toBe("http://localhost:8080/api/v1/auth/login");
|
|
63
|
+
expect(opts.method).toBe("POST");
|
|
64
|
+
expect(JSON.parse(opts.body)).toEqual({
|
|
65
|
+
username: "alice",
|
|
66
|
+
password: "secret",
|
|
67
|
+
});
|
|
68
|
+
expect("access_token" in result && result.access_token).toBe("at-1");
|
|
69
|
+
expect(api.currentAccessToken).toBe("at-1");
|
|
70
|
+
});
|
|
71
|
+
it("register sends POST and sets tokens", async () => {
|
|
72
|
+
// First call: getChallenge()
|
|
73
|
+
const challengeResponse = { challenge: "test-challenge", difficulty: 0 };
|
|
74
|
+
// Second call: register POST
|
|
75
|
+
const authResponse = {
|
|
76
|
+
access_token: "at-reg",
|
|
77
|
+
refresh_token: "rt-reg",
|
|
78
|
+
user: { id: "uuid-2", username: "bob" },
|
|
79
|
+
};
|
|
80
|
+
fetchMock
|
|
81
|
+
.mockResolvedValueOnce(mockResponse(challengeResponse))
|
|
82
|
+
.mockResolvedValueOnce(mockResponse(authResponse));
|
|
83
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
84
|
+
const result = await api.register({
|
|
85
|
+
username: "bob",
|
|
86
|
+
password: "password123",
|
|
87
|
+
identity_key: "key1",
|
|
88
|
+
signed_prekey: "key2",
|
|
89
|
+
signed_prekey_signature: "sig1",
|
|
90
|
+
one_time_prekeys: [],
|
|
91
|
+
});
|
|
92
|
+
// First call is GET /challenge, second is POST /register
|
|
93
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
94
|
+
const [challengeUrl] = fetchMock.mock.calls[0];
|
|
95
|
+
expect(challengeUrl).toBe("http://localhost:8080/api/v1/auth/challenge");
|
|
96
|
+
const [regUrl, regOpts] = fetchMock.mock.calls[1];
|
|
97
|
+
expect(regUrl).toBe("http://localhost:8080/api/v1/auth/register");
|
|
98
|
+
expect(regOpts.method).toBe("POST");
|
|
99
|
+
expect(result.user.username).toBe("bob");
|
|
100
|
+
expect(api.currentAccessToken).toBe("at-reg");
|
|
101
|
+
});
|
|
102
|
+
it("logout clears tokens", async () => {
|
|
103
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
104
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
105
|
+
api.setTokens("at", "rt");
|
|
106
|
+
await api.logout();
|
|
107
|
+
expect(api.currentAccessToken).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
// ── Auth header ─────────────────────────────────────────
|
|
111
|
+
describe("auth header", () => {
|
|
112
|
+
it("includes Bearer token when set", async () => {
|
|
113
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
114
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
115
|
+
api.setTokens("my-token", "rt");
|
|
116
|
+
await api.listServers();
|
|
117
|
+
const [, opts] = fetchMock.mock.calls[0];
|
|
118
|
+
expect(opts.headers["Authorization"]).toBe("Bearer my-token");
|
|
119
|
+
});
|
|
120
|
+
it("omits auth header when no token", async () => {
|
|
121
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
122
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
123
|
+
// Don't set tokens
|
|
124
|
+
// listServers will still make the request (server would return 401)
|
|
125
|
+
try {
|
|
126
|
+
await api.listServers();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// ignore
|
|
130
|
+
}
|
|
131
|
+
const [, opts] = fetchMock.mock.calls[0];
|
|
132
|
+
expect(opts.headers["Authorization"]).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
// ── Error handling ──────────────────────────────────────
|
|
136
|
+
describe("error handling", () => {
|
|
137
|
+
it("non-ok response throws HavenApiError with status", async () => {
|
|
138
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ error: "Forbidden", status: 403 }, 403));
|
|
139
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
140
|
+
api.setTokens("tok", "rt");
|
|
141
|
+
try {
|
|
142
|
+
await api.listServers();
|
|
143
|
+
expect.unreachable("Should have thrown");
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
expect(e).toBeInstanceOf(HavenApiError);
|
|
147
|
+
expect(e.status).toBe(403);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
it("401 triggers onTokenExpired callback", async () => {
|
|
151
|
+
const onExpired = vi.fn();
|
|
152
|
+
fetchMock.mockResolvedValue(mockResponse({ error: "Unauthorized", status: 401 }, 401));
|
|
153
|
+
const api = new HavenApi({
|
|
154
|
+
baseUrl: "http://localhost:8080",
|
|
155
|
+
onTokenExpired: onExpired,
|
|
156
|
+
});
|
|
157
|
+
api.setTokens("old-token", "rt");
|
|
158
|
+
await expect(api.listServers()).rejects.toThrow();
|
|
159
|
+
expect(onExpired).toHaveBeenCalledOnce();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
// ── Server endpoints ────────────────────────────────────
|
|
163
|
+
describe("servers", () => {
|
|
164
|
+
it("listServers sends GET", async () => {
|
|
165
|
+
fetchMock.mockResolvedValueOnce(mockResponse([{ id: "s1", encrypted_meta: "bWV0YQ==" }]));
|
|
166
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
167
|
+
api.setTokens("tok", "rt");
|
|
168
|
+
const servers = await api.listServers();
|
|
169
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
170
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers");
|
|
171
|
+
expect(opts.method).toBe("GET");
|
|
172
|
+
expect(servers).toHaveLength(1);
|
|
173
|
+
});
|
|
174
|
+
it("createServer sends POST with encrypted_meta", async () => {
|
|
175
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "s2", encrypted_meta: "dGVzdA==" }));
|
|
176
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
177
|
+
api.setTokens("tok", "rt");
|
|
178
|
+
await api.createServer({ encrypted_meta: "dGVzdA==" });
|
|
179
|
+
const [, opts] = fetchMock.mock.calls[0];
|
|
180
|
+
expect(opts.method).toBe("POST");
|
|
181
|
+
expect(JSON.parse(opts.body)).toEqual({
|
|
182
|
+
encrypted_meta: "dGVzdA==",
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
// ── Channel endpoints ───────────────────────────────────
|
|
187
|
+
describe("channels", () => {
|
|
188
|
+
it("createChannel sends POST to server channels endpoint", async () => {
|
|
189
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "ch1", server_id: "s1" }));
|
|
190
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
191
|
+
api.setTokens("tok", "rt");
|
|
192
|
+
await api.createChannel("s1", { encrypted_meta: "Y2hhbg==" });
|
|
193
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
194
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/channels");
|
|
195
|
+
expect(opts.method).toBe("POST");
|
|
196
|
+
});
|
|
197
|
+
it("deleteChannel sends DELETE", async () => {
|
|
198
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
199
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
200
|
+
api.setTokens("tok", "rt");
|
|
201
|
+
await api.deleteChannel("ch1");
|
|
202
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
203
|
+
expect(url).toBe("http://localhost:8080/api/v1/channels/ch1");
|
|
204
|
+
expect(opts.method).toBe("DELETE");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
// ── Friends endpoints ───────────────────────────────────
|
|
208
|
+
describe("friends", () => {
|
|
209
|
+
it("sendFriendRequest sends POST", async () => {
|
|
210
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "f1", status: "pending" }));
|
|
211
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
212
|
+
api.setTokens("tok", "rt");
|
|
213
|
+
const result = await api.sendFriendRequest({ username: "carol" });
|
|
214
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
215
|
+
expect(url).toBe("http://localhost:8080/api/v1/friends/request");
|
|
216
|
+
expect(opts.method).toBe("POST");
|
|
217
|
+
expect(result.status).toBe("pending");
|
|
218
|
+
});
|
|
219
|
+
it("acceptFriendRequest sends POST", async () => {
|
|
220
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "f1", status: "accepted" }));
|
|
221
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
222
|
+
api.setTokens("tok", "rt");
|
|
223
|
+
await api.acceptFriendRequest("f1");
|
|
224
|
+
const [url] = fetchMock.mock.calls[0];
|
|
225
|
+
expect(url).toBe("http://localhost:8080/api/v1/friends/f1/accept");
|
|
226
|
+
});
|
|
227
|
+
it("listFriends sends GET", async () => {
|
|
228
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
229
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
230
|
+
api.setTokens("tok", "rt");
|
|
231
|
+
const friends = await api.listFriends();
|
|
232
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
233
|
+
expect(url).toBe("http://localhost:8080/api/v1/friends");
|
|
234
|
+
expect(opts.method).toBe("GET");
|
|
235
|
+
expect(friends).toEqual([]);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
// ── URL construction ────────────────────────────────────
|
|
239
|
+
describe("URL construction", () => {
|
|
240
|
+
it("strips trailing slash from baseUrl", async () => {
|
|
241
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
242
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080/" });
|
|
243
|
+
api.setTokens("tok", "rt");
|
|
244
|
+
await api.listServers();
|
|
245
|
+
const [url] = fetchMock.mock.calls[0];
|
|
246
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers");
|
|
247
|
+
});
|
|
248
|
+
it("getMessages constructs query params", async () => {
|
|
249
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
250
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
251
|
+
api.setTokens("tok", "rt");
|
|
252
|
+
await api.getMessages("ch1", { before: "msg-99", limit: 25 });
|
|
253
|
+
const [url] = fetchMock.mock.calls[0];
|
|
254
|
+
expect(url).toContain("/api/v1/channels/ch1/messages?");
|
|
255
|
+
expect(url).toContain("before=msg-99");
|
|
256
|
+
expect(url).toContain("limit=25");
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
// ── Roles ───────────────────────────────────────────────
|
|
260
|
+
describe("roles", () => {
|
|
261
|
+
it("createRole sends POST", async () => {
|
|
262
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "r1", name: "Admin" }));
|
|
263
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
264
|
+
api.setTokens("tok", "rt");
|
|
265
|
+
await api.createRole("s1", { name: "Admin", permissions: "8" });
|
|
266
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
267
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/roles");
|
|
268
|
+
expect(opts.method).toBe("POST");
|
|
269
|
+
});
|
|
270
|
+
it("assignRole sends PUT", async () => {
|
|
271
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
272
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
273
|
+
api.setTokens("tok", "rt");
|
|
274
|
+
await api.assignRole("s1", "u1", { role_id: "r1" });
|
|
275
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
276
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/members/u1/roles");
|
|
277
|
+
expect(opts.method).toBe("PUT");
|
|
278
|
+
});
|
|
279
|
+
it("updateRole sends PUT with correct path", async () => {
|
|
280
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "r1", name: "Mod" }));
|
|
281
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
282
|
+
api.setTokens("tok", "rt");
|
|
283
|
+
await api.updateRole("s1", "r1", { name: "Mod" });
|
|
284
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
285
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/roles/r1");
|
|
286
|
+
expect(opts.method).toBe("PUT");
|
|
287
|
+
});
|
|
288
|
+
it("deleteRole sends DELETE", async () => {
|
|
289
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
290
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
291
|
+
api.setTokens("tok", "rt");
|
|
292
|
+
await api.deleteRole("s1", "r1");
|
|
293
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
294
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/roles/r1");
|
|
295
|
+
expect(opts.method).toBe("DELETE");
|
|
296
|
+
});
|
|
297
|
+
it("unassignRole sends DELETE with role in path", async () => {
|
|
298
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
299
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
300
|
+
api.setTokens("tok", "rt");
|
|
301
|
+
await api.unassignRole("s1", "u1", "r1");
|
|
302
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
303
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/members/u1/roles/r1");
|
|
304
|
+
expect(opts.method).toBe("DELETE");
|
|
305
|
+
});
|
|
306
|
+
it("listRoles sends GET", async () => {
|
|
307
|
+
fetchMock.mockResolvedValueOnce(mockResponse([{ id: "r1", name: "Admin" }]));
|
|
308
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
309
|
+
api.setTokens("tok", "rt");
|
|
310
|
+
const roles = await api.listRoles("s1");
|
|
311
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
312
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/roles");
|
|
313
|
+
expect(opts.method).toBe("GET");
|
|
314
|
+
expect(roles).toHaveLength(1);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
// ── Pins ────────────────────────────────────────────────
|
|
318
|
+
describe("pins", () => {
|
|
319
|
+
it("getPinnedMessages sends GET to /pins", async () => {
|
|
320
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
321
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
322
|
+
api.setTokens("tok", "rt");
|
|
323
|
+
const pins = await api.getPinnedMessages("ch1");
|
|
324
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
325
|
+
expect(url).toBe("http://localhost:8080/api/v1/channels/ch1/pins");
|
|
326
|
+
expect(opts.method).toBe("GET");
|
|
327
|
+
expect(pins).toEqual([]);
|
|
328
|
+
});
|
|
329
|
+
it("getPinnedMessageIds sends GET to /pin-ids", async () => {
|
|
330
|
+
fetchMock.mockResolvedValueOnce(mockResponse(["id1", "id2"]));
|
|
331
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
332
|
+
api.setTokens("tok", "rt");
|
|
333
|
+
const ids = await api.getPinnedMessageIds("ch1");
|
|
334
|
+
const [url] = fetchMock.mock.calls[0];
|
|
335
|
+
expect(url).toBe("http://localhost:8080/api/v1/channels/ch1/pin-ids");
|
|
336
|
+
expect(ids).toEqual(["id1", "id2"]);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
// ── Reports ─────────────────────────────────────────────
|
|
340
|
+
describe("reports", () => {
|
|
341
|
+
it("reportMessage sends POST to /reports", async () => {
|
|
342
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "rep1", status: "pending" }));
|
|
343
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
344
|
+
api.setTokens("tok", "rt");
|
|
345
|
+
const result = await api.reportMessage({
|
|
346
|
+
message_id: "msg1",
|
|
347
|
+
channel_id: "ch1",
|
|
348
|
+
reason: "spam content here",
|
|
349
|
+
});
|
|
350
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
351
|
+
expect(url).toBe("http://localhost:8080/api/v1/reports");
|
|
352
|
+
expect(opts.method).toBe("POST");
|
|
353
|
+
expect(JSON.parse(opts.body)).toEqual({
|
|
354
|
+
message_id: "msg1",
|
|
355
|
+
channel_id: "ch1",
|
|
356
|
+
reason: "spam content here",
|
|
357
|
+
});
|
|
358
|
+
expect(result.status).toBe("pending");
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
// ── Reactions ───────────────────────────────────────────
|
|
362
|
+
describe("reactions", () => {
|
|
363
|
+
it("getChannelReactions sends GET", async () => {
|
|
364
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
365
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
366
|
+
api.setTokens("tok", "rt");
|
|
367
|
+
const reactions = await api.getChannelReactions("ch1");
|
|
368
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
369
|
+
expect(url).toBe("http://localhost:8080/api/v1/channels/ch1/reactions");
|
|
370
|
+
expect(opts.method).toBe("GET");
|
|
371
|
+
expect(reactions).toEqual([]);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
// ── User profiles ───────────────────────────────────────
|
|
375
|
+
describe("user profiles", () => {
|
|
376
|
+
it("getUserProfile sends GET without server_id", async () => {
|
|
377
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "u1", username: "alice" }));
|
|
378
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
379
|
+
api.setTokens("tok", "rt");
|
|
380
|
+
await api.getUserProfile("u1");
|
|
381
|
+
const [url] = fetchMock.mock.calls[0];
|
|
382
|
+
expect(url).toBe("http://localhost:8080/api/v1/users/u1/profile");
|
|
383
|
+
});
|
|
384
|
+
it("getUserProfile includes server_id query param", async () => {
|
|
385
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "u1", username: "alice" }));
|
|
386
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
387
|
+
api.setTokens("tok", "rt");
|
|
388
|
+
await api.getUserProfile("u1", "s1");
|
|
389
|
+
const [url] = fetchMock.mock.calls[0];
|
|
390
|
+
expect(url).toBe("http://localhost:8080/api/v1/users/u1/profile?server_id=s1");
|
|
391
|
+
});
|
|
392
|
+
it("updateProfile sends PUT", async () => {
|
|
393
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "u1", display_name: "Alice" }));
|
|
394
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
395
|
+
api.setTokens("tok", "rt");
|
|
396
|
+
await api.updateProfile({ display_name: "Alice" });
|
|
397
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
398
|
+
expect(url).toBe("http://localhost:8080/api/v1/users/profile");
|
|
399
|
+
expect(opts.method).toBe("PUT");
|
|
400
|
+
});
|
|
401
|
+
it("getUserByUsername encodes username in query", async () => {
|
|
402
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "u1", username: "test user" }));
|
|
403
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
404
|
+
api.setTokens("tok", "rt");
|
|
405
|
+
await api.getUserByUsername("test user");
|
|
406
|
+
const [url] = fetchMock.mock.calls[0];
|
|
407
|
+
expect(url).toContain("/api/v1/users/search?username=test%20user");
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
// ── Blocked users ───────────────────────────────────────
|
|
411
|
+
describe("blocked users", () => {
|
|
412
|
+
it("blockUser sends POST", async () => {
|
|
413
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
414
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
415
|
+
api.setTokens("tok", "rt");
|
|
416
|
+
await api.blockUser("u2");
|
|
417
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
418
|
+
expect(url).toBe("http://localhost:8080/api/v1/users/u2/block");
|
|
419
|
+
expect(opts.method).toBe("POST");
|
|
420
|
+
});
|
|
421
|
+
it("unblockUser sends DELETE", async () => {
|
|
422
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
423
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
424
|
+
api.setTokens("tok", "rt");
|
|
425
|
+
await api.unblockUser("u2");
|
|
426
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
427
|
+
expect(url).toBe("http://localhost:8080/api/v1/users/u2/block");
|
|
428
|
+
expect(opts.method).toBe("DELETE");
|
|
429
|
+
});
|
|
430
|
+
it("getBlockedUsers sends GET", async () => {
|
|
431
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
432
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
433
|
+
api.setTokens("tok", "rt");
|
|
434
|
+
const blocked = await api.getBlockedUsers();
|
|
435
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
436
|
+
expect(url).toBe("http://localhost:8080/api/v1/users/blocked");
|
|
437
|
+
expect(opts.method).toBe("GET");
|
|
438
|
+
expect(blocked).toEqual([]);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
// ── Bans ────────────────────────────────────────────────
|
|
442
|
+
describe("bans", () => {
|
|
443
|
+
it("banMember sends POST with reason", async () => {
|
|
444
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "ban1", user_id: "u2" }));
|
|
445
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
446
|
+
api.setTokens("tok", "rt");
|
|
447
|
+
await api.banMember("s1", "u2", { reason: "rule violation" });
|
|
448
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
449
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/bans/u2");
|
|
450
|
+
expect(opts.method).toBe("POST");
|
|
451
|
+
expect(JSON.parse(opts.body)).toEqual({ reason: "rule violation" });
|
|
452
|
+
});
|
|
453
|
+
it("revokeBan sends DELETE", async () => {
|
|
454
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
455
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
456
|
+
api.setTokens("tok", "rt");
|
|
457
|
+
await api.revokeBan("s1", "u2");
|
|
458
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
459
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/bans/u2");
|
|
460
|
+
expect(opts.method).toBe("DELETE");
|
|
461
|
+
});
|
|
462
|
+
it("listBans sends GET", async () => {
|
|
463
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
464
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
465
|
+
api.setTokens("tok", "rt");
|
|
466
|
+
const bans = await api.listBans("s1");
|
|
467
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
468
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/bans");
|
|
469
|
+
expect(opts.method).toBe("GET");
|
|
470
|
+
expect(bans).toEqual([]);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
// ── DMs ─────────────────────────────────────────────────
|
|
474
|
+
describe("DMs", () => {
|
|
475
|
+
it("listDmChannels sends GET to /dm", async () => {
|
|
476
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
477
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
478
|
+
api.setTokens("tok", "rt");
|
|
479
|
+
const dms = await api.listDmChannels();
|
|
480
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
481
|
+
expect(url).toBe("http://localhost:8080/api/v1/dm");
|
|
482
|
+
expect(opts.method).toBe("GET");
|
|
483
|
+
expect(dms).toEqual([]);
|
|
484
|
+
});
|
|
485
|
+
it("createDm sends POST to /dm", async () => {
|
|
486
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "ch1", channel_type: "dm" }));
|
|
487
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
488
|
+
api.setTokens("tok", "rt");
|
|
489
|
+
await api.createDm({ target_user_id: "u2", encrypted_meta: "bWV0YQ==" });
|
|
490
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
491
|
+
expect(url).toBe("http://localhost:8080/api/v1/dm");
|
|
492
|
+
expect(opts.method).toBe("POST");
|
|
493
|
+
});
|
|
494
|
+
it("createGroupDm sends POST to /dm/group", async () => {
|
|
495
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "ch2", channel_type: "group_dm" }));
|
|
496
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
497
|
+
api.setTokens("tok", "rt");
|
|
498
|
+
await api.createGroupDm({ member_ids: ["u2", "u3"], encrypted_meta: "bWV0YQ==" });
|
|
499
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
500
|
+
expect(url).toBe("http://localhost:8080/api/v1/dm/group");
|
|
501
|
+
expect(opts.method).toBe("POST");
|
|
502
|
+
});
|
|
503
|
+
it("listDmRequests sends GET", async () => {
|
|
504
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
505
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
506
|
+
api.setTokens("tok", "rt");
|
|
507
|
+
const reqs = await api.listDmRequests();
|
|
508
|
+
const [url] = fetchMock.mock.calls[0];
|
|
509
|
+
expect(url).toBe("http://localhost:8080/api/v1/dm/requests");
|
|
510
|
+
expect(reqs).toEqual([]);
|
|
511
|
+
});
|
|
512
|
+
it("handleDmRequest sends POST with action", async () => {
|
|
513
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
514
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
515
|
+
api.setTokens("tok", "rt");
|
|
516
|
+
await api.handleDmRequest("ch1", { action: "accept" });
|
|
517
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
518
|
+
expect(url).toBe("http://localhost:8080/api/v1/dm/ch1/request");
|
|
519
|
+
expect(opts.method).toBe("POST");
|
|
520
|
+
expect(JSON.parse(opts.body)).toEqual({ action: "accept" });
|
|
521
|
+
});
|
|
522
|
+
it("updateDmPrivacy sends PUT", async () => {
|
|
523
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
524
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
525
|
+
api.setTokens("tok", "rt");
|
|
526
|
+
await api.updateDmPrivacy({ dm_privacy: "friends_only" });
|
|
527
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
528
|
+
expect(url).toBe("http://localhost:8080/api/v1/users/dm-privacy");
|
|
529
|
+
expect(opts.method).toBe("PUT");
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
// ── Channel members ─────────────────────────────────────
|
|
533
|
+
describe("channel members", () => {
|
|
534
|
+
it("listChannelMembers sends GET", async () => {
|
|
535
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
536
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
537
|
+
api.setTokens("tok", "rt");
|
|
538
|
+
const members = await api.listChannelMembers("ch1");
|
|
539
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
540
|
+
expect(url).toBe("http://localhost:8080/api/v1/channels/ch1/members");
|
|
541
|
+
expect(opts.method).toBe("GET");
|
|
542
|
+
expect(members).toEqual([]);
|
|
543
|
+
});
|
|
544
|
+
it("leaveChannel sends DELETE", async () => {
|
|
545
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
546
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
547
|
+
api.setTokens("tok", "rt");
|
|
548
|
+
await api.leaveChannel("ch1");
|
|
549
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
550
|
+
expect(url).toBe("http://localhost:8080/api/v1/channels/ch1/leave");
|
|
551
|
+
expect(opts.method).toBe("DELETE");
|
|
552
|
+
});
|
|
553
|
+
it("addGroupMember sends POST with user_id", async () => {
|
|
554
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
555
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
556
|
+
api.setTokens("tok", "rt");
|
|
557
|
+
await api.addGroupMember("ch1", "u2");
|
|
558
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
559
|
+
expect(url).toBe("http://localhost:8080/api/v1/channels/ch1/members");
|
|
560
|
+
expect(opts.method).toBe("POST");
|
|
561
|
+
expect(JSON.parse(opts.body)).toEqual({ user_id: "u2" });
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
// ── Channel updates ─────────────────────────────────────
|
|
565
|
+
describe("channel updates", () => {
|
|
566
|
+
it("updateChannel sends PUT", async () => {
|
|
567
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "ch1", encrypted_meta: "dXBkYXRlZA==" }));
|
|
568
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
569
|
+
api.setTokens("tok", "rt");
|
|
570
|
+
await api.updateChannel("ch1", { encrypted_meta: "dXBkYXRlZA==" });
|
|
571
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
572
|
+
expect(url).toBe("http://localhost:8080/api/v1/channels/ch1");
|
|
573
|
+
expect(opts.method).toBe("PUT");
|
|
574
|
+
});
|
|
575
|
+
it("joinChannel sends POST to /join", async () => {
|
|
576
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
577
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
578
|
+
api.setTokens("tok", "rt");
|
|
579
|
+
await api.joinChannel("ch1");
|
|
580
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
581
|
+
expect(url).toBe("http://localhost:8080/api/v1/channels/ch1/join");
|
|
582
|
+
expect(opts.method).toBe("POST");
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
// ── Overwrites ──────────────────────────────────────────
|
|
586
|
+
describe("overwrites", () => {
|
|
587
|
+
it("listOverwrites sends GET", async () => {
|
|
588
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
589
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
590
|
+
api.setTokens("tok", "rt");
|
|
591
|
+
const overwrites = await api.listOverwrites("ch1");
|
|
592
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
593
|
+
expect(url).toBe("http://localhost:8080/api/v1/channels/ch1/overwrites");
|
|
594
|
+
expect(opts.method).toBe("GET");
|
|
595
|
+
expect(overwrites).toEqual([]);
|
|
596
|
+
});
|
|
597
|
+
it("setOverwrite sends PUT", async () => {
|
|
598
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "ow1", channel_id: "ch1" }));
|
|
599
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
600
|
+
api.setTokens("tok", "rt");
|
|
601
|
+
await api.setOverwrite("ch1", {
|
|
602
|
+
target_type: "role",
|
|
603
|
+
target_id: "r1",
|
|
604
|
+
allow_bits: "8",
|
|
605
|
+
deny_bits: "0",
|
|
606
|
+
});
|
|
607
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
608
|
+
expect(url).toBe("http://localhost:8080/api/v1/channels/ch1/overwrites");
|
|
609
|
+
expect(opts.method).toBe("PUT");
|
|
610
|
+
});
|
|
611
|
+
it("deleteOverwrite sends DELETE with target in path", async () => {
|
|
612
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
613
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
614
|
+
api.setTokens("tok", "rt");
|
|
615
|
+
await api.deleteOverwrite("ch1", "role", "r1");
|
|
616
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
617
|
+
expect(url).toBe("http://localhost:8080/api/v1/channels/ch1/overwrites/role/r1");
|
|
618
|
+
expect(opts.method).toBe("DELETE");
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
// ── Server members & invites ────────────────────────────
|
|
622
|
+
describe("server members and invites", () => {
|
|
623
|
+
it("listServerMembers sends GET", async () => {
|
|
624
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
625
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
626
|
+
api.setTokens("tok", "rt");
|
|
627
|
+
const members = await api.listServerMembers("s1");
|
|
628
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
629
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/members?limit=100");
|
|
630
|
+
expect(opts.method).toBe("GET");
|
|
631
|
+
expect(members).toEqual([]);
|
|
632
|
+
});
|
|
633
|
+
it("kickMember sends DELETE", async () => {
|
|
634
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
635
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
636
|
+
api.setTokens("tok", "rt");
|
|
637
|
+
await api.kickMember("s1", "u2");
|
|
638
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
639
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/members/u2");
|
|
640
|
+
expect(opts.method).toBe("DELETE");
|
|
641
|
+
});
|
|
642
|
+
it("createInvite sends POST", async () => {
|
|
643
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "inv1", code: "abc123" }));
|
|
644
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
645
|
+
api.setTokens("tok", "rt");
|
|
646
|
+
await api.createInvite("s1", { expires_in_hours: 24 });
|
|
647
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
648
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/invites");
|
|
649
|
+
expect(opts.method).toBe("POST");
|
|
650
|
+
});
|
|
651
|
+
it("listInvites sends GET", async () => {
|
|
652
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
653
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
654
|
+
api.setTokens("tok", "rt");
|
|
655
|
+
const invites = await api.listInvites("s1");
|
|
656
|
+
const [url] = fetchMock.mock.calls[0];
|
|
657
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/invites");
|
|
658
|
+
expect(invites).toEqual([]);
|
|
659
|
+
});
|
|
660
|
+
it("deleteInvite sends DELETE", async () => {
|
|
661
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
662
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
663
|
+
api.setTokens("tok", "rt");
|
|
664
|
+
await api.deleteInvite("s1", "inv1");
|
|
665
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
666
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/invites/inv1");
|
|
667
|
+
expect(opts.method).toBe("DELETE");
|
|
668
|
+
});
|
|
669
|
+
it("joinByInvite sends POST with invite code", async () => {
|
|
670
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "s1", encrypted_meta: "bWV0YQ==" }));
|
|
671
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
672
|
+
api.setTokens("tok", "rt");
|
|
673
|
+
await api.joinByInvite("abc123");
|
|
674
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
675
|
+
expect(url).toBe("http://localhost:8080/api/v1/invites/abc123/join");
|
|
676
|
+
expect(opts.method).toBe("POST");
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
// ── Categories ──────────────────────────────────────────
|
|
680
|
+
describe("categories", () => {
|
|
681
|
+
it("listCategories sends GET", async () => {
|
|
682
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
683
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
684
|
+
api.setTokens("tok", "rt");
|
|
685
|
+
const cats = await api.listCategories("s1");
|
|
686
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
687
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/categories");
|
|
688
|
+
expect(opts.method).toBe("GET");
|
|
689
|
+
expect(cats).toEqual([]);
|
|
690
|
+
});
|
|
691
|
+
it("createCategory sends POST", async () => {
|
|
692
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "cat1", name: "General" }));
|
|
693
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
694
|
+
api.setTokens("tok", "rt");
|
|
695
|
+
await api.createCategory("s1", { name: "General" });
|
|
696
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
697
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/categories");
|
|
698
|
+
expect(opts.method).toBe("POST");
|
|
699
|
+
});
|
|
700
|
+
it("updateCategory sends PUT", async () => {
|
|
701
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "cat1", name: "Updated" }));
|
|
702
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
703
|
+
api.setTokens("tok", "rt");
|
|
704
|
+
await api.updateCategory("s1", "cat1", { name: "Updated" });
|
|
705
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
706
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/categories/cat1");
|
|
707
|
+
expect(opts.method).toBe("PUT");
|
|
708
|
+
});
|
|
709
|
+
it("deleteCategory sends DELETE", async () => {
|
|
710
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
711
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
712
|
+
api.setTokens("tok", "rt");
|
|
713
|
+
await api.deleteCategory("s1", "cat1");
|
|
714
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
715
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/categories/cat1");
|
|
716
|
+
expect(opts.method).toBe("DELETE");
|
|
717
|
+
});
|
|
718
|
+
it("reorderCategories sends PUT", async () => {
|
|
719
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
720
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
721
|
+
api.setTokens("tok", "rt");
|
|
722
|
+
await api.reorderCategories("s1", { order: [{ id: "cat2", position: 0 }, { id: "cat1", position: 1 }] });
|
|
723
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
724
|
+
expect(url).toBe("http://localhost:8080/api/v1/servers/s1/categories/reorder");
|
|
725
|
+
expect(opts.method).toBe("PUT");
|
|
726
|
+
});
|
|
727
|
+
it("setChannelCategory sends PUT", async () => {
|
|
728
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "ch1", category_id: "cat1" }));
|
|
729
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
730
|
+
api.setTokens("tok", "rt");
|
|
731
|
+
await api.setChannelCategory("ch1", { category_id: "cat1" });
|
|
732
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
733
|
+
expect(url).toBe("http://localhost:8080/api/v1/channels/ch1/category");
|
|
734
|
+
expect(opts.method).toBe("PUT");
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
// ── Friends extended ────────────────────────────────────
|
|
738
|
+
describe("friends extended", () => {
|
|
739
|
+
it("declineFriendRequest sends POST", async () => {
|
|
740
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
741
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
742
|
+
api.setTokens("tok", "rt");
|
|
743
|
+
await api.declineFriendRequest("f1");
|
|
744
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
745
|
+
expect(url).toBe("http://localhost:8080/api/v1/friends/f1/decline");
|
|
746
|
+
expect(opts.method).toBe("POST");
|
|
747
|
+
});
|
|
748
|
+
it("removeFriend sends DELETE", async () => {
|
|
749
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
750
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
751
|
+
api.setTokens("tok", "rt");
|
|
752
|
+
await api.removeFriend("f1");
|
|
753
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
754
|
+
expect(url).toBe("http://localhost:8080/api/v1/friends/f1");
|
|
755
|
+
expect(opts.method).toBe("DELETE");
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
// ── Auth extended ───────────────────────────────────────
|
|
759
|
+
describe("auth extended", () => {
|
|
760
|
+
it("changePassword sends PUT", async () => {
|
|
761
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
762
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
763
|
+
api.setTokens("tok", "rt");
|
|
764
|
+
await api.changePassword({
|
|
765
|
+
current_password: "old",
|
|
766
|
+
new_password: "new123",
|
|
767
|
+
});
|
|
768
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
769
|
+
expect(url).toBe("http://localhost:8080/api/v1/auth/password");
|
|
770
|
+
expect(opts.method).toBe("PUT");
|
|
771
|
+
});
|
|
772
|
+
it("refresh sends POST with refresh token", async () => {
|
|
773
|
+
fetchMock.mockResolvedValueOnce(mockResponse({
|
|
774
|
+
access_token: "new-at",
|
|
775
|
+
refresh_token: "new-rt",
|
|
776
|
+
user: { id: "u1" },
|
|
777
|
+
}));
|
|
778
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
779
|
+
api.setTokens("old-at", "old-rt");
|
|
780
|
+
const result = await api.refresh();
|
|
781
|
+
const [, opts] = fetchMock.mock.calls[0];
|
|
782
|
+
expect(JSON.parse(opts.body)).toEqual({ refresh_token: "old-rt" });
|
|
783
|
+
expect(result.access_token).toBe("new-at");
|
|
784
|
+
expect(api.currentAccessToken).toBe("new-at");
|
|
785
|
+
});
|
|
786
|
+
it("refresh throws without refresh token", async () => {
|
|
787
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
788
|
+
await expect(api.refresh()).rejects.toThrow("No refresh token");
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
// ── Keys ────────────────────────────────────────────────
|
|
792
|
+
describe("keys", () => {
|
|
793
|
+
it("getKeyBundle sends GET", async () => {
|
|
794
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ identity_key: "key1", signed_prekey: "key2" }));
|
|
795
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
796
|
+
api.setTokens("tok", "rt");
|
|
797
|
+
const bundle = await api.getKeyBundle("u1");
|
|
798
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
799
|
+
expect(url).toBe("http://localhost:8080/api/v1/users/u1/keys");
|
|
800
|
+
expect(opts.method).toBe("GET");
|
|
801
|
+
expect(bundle.identity_key).toBe("key1");
|
|
802
|
+
});
|
|
803
|
+
it("uploadPreKeys sends POST", async () => {
|
|
804
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
805
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
806
|
+
api.setTokens("tok", "rt");
|
|
807
|
+
await api.uploadPreKeys({ prekeys: ["pk1", "pk2"] });
|
|
808
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
809
|
+
expect(url).toBe("http://localhost:8080/api/v1/keys/prekeys");
|
|
810
|
+
expect(opts.method).toBe("POST");
|
|
811
|
+
});
|
|
812
|
+
it("getPreKeyCount sends GET", async () => {
|
|
813
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ count: 42 }));
|
|
814
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
815
|
+
api.setTokens("tok", "rt");
|
|
816
|
+
const result = await api.getPreKeyCount();
|
|
817
|
+
const [url] = fetchMock.mock.calls[0];
|
|
818
|
+
expect(url).toBe("http://localhost:8080/api/v1/keys/prekeys/count");
|
|
819
|
+
expect(result.count).toBe(42);
|
|
820
|
+
});
|
|
821
|
+
it("updateKeys sends PUT", async () => {
|
|
822
|
+
fetchMock.mockResolvedValueOnce(mockResponse(null));
|
|
823
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
824
|
+
api.setTokens("tok", "rt");
|
|
825
|
+
await api.updateKeys({
|
|
826
|
+
identity_key: "new-key",
|
|
827
|
+
signed_prekey: "new-spk",
|
|
828
|
+
signed_prekey_signature: "new-sig",
|
|
829
|
+
});
|
|
830
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
831
|
+
expect(url).toBe("http://localhost:8080/api/v1/keys/identity");
|
|
832
|
+
expect(opts.method).toBe("PUT");
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
// ── Messages ────────────────────────────────────────────
|
|
836
|
+
describe("messages", () => {
|
|
837
|
+
it("sendMessage sends POST to channel messages", async () => {
|
|
838
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ id: "msg1", channel_id: "ch1" }));
|
|
839
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
840
|
+
api.setTokens("tok", "rt");
|
|
841
|
+
await api.sendMessage("ch1", {
|
|
842
|
+
channel_id: "ch1",
|
|
843
|
+
sender_token: "st",
|
|
844
|
+
encrypted_body: "eb",
|
|
845
|
+
has_attachments: false,
|
|
846
|
+
});
|
|
847
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
848
|
+
expect(url).toBe("http://localhost:8080/api/v1/channels/ch1/messages");
|
|
849
|
+
expect(opts.method).toBe("POST");
|
|
850
|
+
});
|
|
851
|
+
it("getMessages with no query params omits query string", async () => {
|
|
852
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
853
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
854
|
+
api.setTokens("tok", "rt");
|
|
855
|
+
await api.getMessages("ch1");
|
|
856
|
+
const [url] = fetchMock.mock.calls[0];
|
|
857
|
+
expect(url).toBe("http://localhost:8080/api/v1/channels/ch1/messages");
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
// ── Presence ────────────────────────────────────────────
|
|
861
|
+
describe("presence", () => {
|
|
862
|
+
it("getPresence sends GET with comma-joined user_ids", async () => {
|
|
863
|
+
fetchMock.mockResolvedValueOnce(mockResponse([]));
|
|
864
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
865
|
+
api.setTokens("tok", "rt");
|
|
866
|
+
await api.getPresence(["u1", "u2", "u3"]);
|
|
867
|
+
const [url] = fetchMock.mock.calls[0];
|
|
868
|
+
expect(url).toBe("http://localhost:8080/api/v1/presence?user_ids=u1,u2,u3");
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
// ── Link preview ────────────────────────────────────────
|
|
872
|
+
describe("link preview", () => {
|
|
873
|
+
it("fetchLinkPreview sends GET with encoded URL", async () => {
|
|
874
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ url: "https://example.com", title: "Example" }));
|
|
875
|
+
const api = new HavenApi({ baseUrl: "http://localhost:8080" });
|
|
876
|
+
api.setTokens("tok", "rt");
|
|
877
|
+
const preview = await api.fetchLinkPreview("https://example.com/path?q=1");
|
|
878
|
+
const [url] = fetchMock.mock.calls[0];
|
|
879
|
+
expect(url).toContain("/api/v1/link-preview?url=");
|
|
880
|
+
expect(url).toContain(encodeURIComponent("https://example.com/path?q=1"));
|
|
881
|
+
expect(preview.title).toBe("Example");
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
//# sourceMappingURL=api.test.js.map
|