refactor(api): ♻️ Refactor all tests to use new client

This commit is contained in:
Jesse Wierzbinski 2025-03-22 17:32:46 +01:00
parent b6373dc185
commit 54e282b03c
No known key found for this signature in database
31 changed files with 1607 additions and 2002 deletions

View file

@ -1,9 +1,9 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { Role } from "@versia/kit/db"; import { Role } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { fakeRequest, getTestUsers } from "~/tests/utils"; import { generateClient, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, deleteUsers } = await getTestUsers(2);
let role: Role; let role: Role;
let higherPriorityRole: Role; let higherPriorityRole: Role;
@ -44,56 +44,44 @@ afterAll(async () => {
// /api/v1/accounts/:id/roles/:role_id // /api/v1/accounts/:id/roles/:role_id
describe("/api/v1/accounts/:id/roles/:role_id", () => { describe("/api/v1/accounts/:id/roles/:role_id", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest( await using client = await generateClient();
`/api/v1/accounts/${users[1].id}/roles/${role.id}`,
{
method: "POST",
},
);
expect(response.status).toBe(401); const { ok, raw } = await client.assignRole(users[1].id, role.id);
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should return 404 if role does not exist", async () => { test("should return 404 if role does not exist", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/accounts/${users[1].id}/roles/00000000-0000-0000-0000-000000000000`,
{ const { ok, raw } = await client.assignRole(
method: "POST", users[1].id,
headers: { "00000000-0000-0000-0000-000000000000",
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
); );
expect(response.status).toBe(404); expect(ok).toBe(false);
expect(raw.status).toBe(404);
}); });
test("should return 404 if user does not exist", async () => { test("should return 404 if user does not exist", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/accounts/00000000-0000-0000-0000-000000000000/roles/${role.id}`,
{ const { ok, raw } = await client.assignRole(
method: "POST", "00000000-0000-0000-0000-000000000000",
headers: { role.id,
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
); );
expect(response.status).toBe(404); expect(ok).toBe(false);
expect(raw.status).toBe(404);
}); });
test("should assign role to user", async () => { test("should assign role to user", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/accounts/${users[1].id}/roles/${role.id}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(204); const { ok } = await client.assignRole(users[1].id, role.id);
expect(ok).toBe(true);
// Check if role was assigned // Check if role was assigned
const userRoles = await Role.getUserRoles(users[1].id, false); const userRoles = await Role.getUserRoles(users[1].id, false);
@ -103,19 +91,16 @@ describe("/api/v1/accounts/:id/roles/:role_id", () => {
}); });
test("should return 403 if user tries to assign role with higher priority", async () => { test("should return 403 if user tries to assign role with higher priority", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/accounts/${users[1].id}/roles/${higherPriorityRole.id}`,
{ const { data, ok, raw } = await client.assignRole(
method: "POST", users[1].id,
headers: { higherPriorityRole.id,
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
); );
expect(response.status).toBe(403); expect(ok).toBe(false);
const output = await response.json(); expect(raw.status).toBe(403);
expect(output).toMatchObject({ expect(data).toMatchObject({
error: "Forbidden", error: "Forbidden",
details: details:
"User with highest role priority 2 cannot assign role with priority 3", "User with highest role priority 2 cannot assign role with priority 3",
@ -123,17 +108,11 @@ describe("/api/v1/accounts/:id/roles/:role_id", () => {
}); });
test("should remove role from user", async () => { test("should remove role from user", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/accounts/${users[1].id}/roles/${role.id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(204); const { ok } = await client.unassignRole(users[1].id, role.id);
expect(ok).toBe(true);
// Check if role was removed // Check if role was removed
const userRoles = await Role.getUserRoles(users[1].id, false); const userRoles = await Role.getUserRoles(users[1].id, false);
@ -145,19 +124,16 @@ describe("/api/v1/accounts/:id/roles/:role_id", () => {
test("should return 403 if user tries to remove role with higher priority", async () => { test("should return 403 if user tries to remove role with higher priority", async () => {
await higherPriorityRole.linkUser(users[1].id); await higherPriorityRole.linkUser(users[1].id);
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/accounts/${users[1].id}/roles/${higherPriorityRole.id}`,
{ const { data, ok, raw } = await client.unassignRole(
method: "DELETE", users[1].id,
headers: { higherPriorityRole.id,
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
); );
expect(response.status).toBe(403); expect(ok).toBe(false);
const output = await response.json(); expect(raw.status).toBe(403);
expect(output).toMatchObject({ expect(data).toMatchObject({
error: "Forbidden", error: "Forbidden",
details: details:
"User with highest role priority 2 cannot remove role with priority 3", "User with highest role priority 2 cannot remove role with priority 3",

View file

@ -1,9 +1,9 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { Role } from "@versia/kit/db"; import { Role } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { fakeRequest, getTestUsers } from "~/tests/utils"; import { generateClient, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, deleteUsers } = await getTestUsers(2);
let role: Role; let role: Role;
beforeAll(async () => { beforeAll(async () => {
@ -29,37 +29,27 @@ afterAll(async () => {
describe("/api/v1/accounts/:id/roles", () => { describe("/api/v1/accounts/:id/roles", () => {
test("should return 404 if user does not exist", async () => { test("should return 404 if user does not exist", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
"/api/v1/accounts/00000000-0000-0000-0000-000000000000/roles",
{ const { data, ok, raw } = await client.getAccountRoles(
method: "GET", "00000000-0000-0000-0000-000000000000",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
); );
expect(response.status).toBe(404); expect(ok).toBe(false);
const output = await response.json(); expect(raw.status).toBe(404);
expect(output).toMatchObject({ expect(data).toMatchObject({
error: "User not found", error: "User not found",
}); });
}); });
test("should return a list of roles for the user", async () => { test("should return a list of roles for the user", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/accounts/${users[0].id}/roles`,
{
method: "GET",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.ok).toBe(true); const { data, ok } = await client.getAccountRoles(users[0].id);
const roles = await response.json();
expect(roles).toContainEqual({ expect(ok).toBe(true);
expect(data).toBeArray();
expect(data).toContainEqual({
id: role.id, id: role.id,
name: "test", name: "test",
permissions: [RolePermissions.ManageRoles], permissions: [RolePermissions.ManageRoles],

View file

@ -2,9 +2,9 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { db } from "@versia/kit/db"; import { db } from "@versia/kit/db";
import { Emojis } from "@versia/kit/tables"; import { Emojis } from "@versia/kit/tables";
import { inArray } from "drizzle-orm"; import { inArray } from "drizzle-orm";
import { fakeRequest, getTestUsers } from "~/tests/utils"; import { generateClient, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, deleteUsers } = await getTestUsers(2);
let id = ""; let id = "";
// Make user 2 an admin // Make user 2 an admin
@ -12,22 +12,14 @@ beforeAll(async () => {
await users[1].update({ isAdmin: true }); await users[1].update({ isAdmin: true });
// Create an emoji // Create an emoji
const response = await fakeRequest("/api/v1/emojis", { await using client = await generateClient(users[1]);
headers: { const { data, ok } = await client.uploadEmoji(
Authorization: `Bearer ${tokens[1].data.accessToken}`, "test",
"Content-Type": "application/json", new URL("https://cdn.versia.social/logo.webp"),
}, );
method: "POST",
body: JSON.stringify({
shortcode: "test",
element: "https://cdn.versia.social/logo.webp",
global: true,
}),
});
expect(response.ok).toBe(true); expect(ok).toBe(true);
const emoji = await response.json(); id = data.id;
id = emoji.id;
}); });
afterAll(async () => { afterAll(async () => {
@ -41,124 +33,95 @@ afterAll(async () => {
// /api/v1/emojis/:id (PATCH, DELETE, GET) // /api/v1/emojis/:id (PATCH, DELETE, GET)
describe("/api/v1/emojis/:id", () => { describe("/api/v1/emojis/:id", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(`/api/v1/emojis/${id}`, { await using client = await generateClient();
method: "GET",
});
expect(response.status).toBe(401); const { ok, raw } = await client.getEmoji(id);
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should return 404 if emoji does not exist", async () => { test("should return 404 if emoji does not exist", async () => {
const response = await fakeRequest( await using client = await generateClient(users[1]);
"/api/v1/emojis/00000000-0000-0000-0000-000000000000",
{ const { ok, raw } = await client.getEmoji(
headers: { "00000000-0000-0000-0000-000000000000",
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
method: "GET",
},
); );
expect(response.status).toBe(404); expect(ok).toBe(false);
expect(raw.status).toBe(404);
}); });
test("should not work if the user is trying to update an emoji they don't own", async () => { test("should not work if the user is trying to update an emoji they don't own", async () => {
const response = await fakeRequest(`/api/v1/emojis/${id}`, { await using client = await generateClient(users[0]);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`, const { ok, raw } = await client.updateEmoji(id, {
"Content-Type": "application/json",
},
method: "PATCH",
body: JSON.stringify({
shortcode: "test2", shortcode: "test2",
}),
}); });
expect(response.status).toBe(403); expect(ok).toBe(false);
expect(raw.status).toBe(403);
}); });
test("should return the emoji", async () => { test("should return the emoji", async () => {
const response = await fakeRequest(`/api/v1/emojis/${id}`, { await using client = await generateClient(users[1]);
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
method: "GET",
});
expect(response.ok).toBe(true); const { data, ok } = await client.getEmoji(id);
const emoji = await response.json();
expect(emoji.shortcode).toBe("test"); expect(ok).toBe(true);
expect(data.shortcode).toBe("test");
}); });
test("should update the emoji", async () => { test("should update the emoji", async () => {
const response = await fakeRequest(`/api/v1/emojis/${id}`, { await using client = await generateClient(users[1]);
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`, const { data, ok } = await client.updateEmoji(id, {
"Content-Type": "application/json",
},
method: "PATCH",
body: JSON.stringify({
shortcode: "test2", shortcode: "test2",
}),
}); });
expect(response.ok).toBe(true); expect(ok).toBe(true);
const emoji = await response.json(); expect(data.shortcode).toBe("test2");
expect(emoji.shortcode).toBe("test2");
}); });
test("should update the emoji with another url, but keep the shortcode", async () => { test("should update the emoji with another url, but keep the shortcode", async () => {
const response = await fakeRequest(`/api/v1/emojis/${id}`, { await using client = await generateClient(users[1]);
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`, const { data, ok } = await client.updateEmoji(id, {
"Content-Type": "application/json", image: new URL(
}, "https://avatars.githubusercontent.com/u/30842467?v=4",
method: "PATCH", ),
body: JSON.stringify({
element: "https://avatars.githubusercontent.com/u/30842467?v=4",
}),
}); });
expect(response.ok).toBe(true); expect(ok).toBe(true);
const emoji = await response.json(); expect(data.shortcode).toBe("test2");
expect(emoji.shortcode).toBe("test2"); expect(data.url).toContain("/media/proxy/");
}); });
test("should update the emoji to be non-global", async () => { test("should update the emoji to be non-global", async () => {
const response = await fakeRequest(`/api/v1/emojis/${id}`, { await using client = await generateClient(users[1]);
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`, const { data, ok } = await client.updateEmoji(id, {
"Content-Type": "application/json",
},
method: "PATCH",
body: JSON.stringify({
global: false, global: false,
}),
}); });
expect(response.ok).toBe(true); expect(ok).toBe(true);
expect(data.global).toBe(false);
// Check if the other user can see it // Check if the other user can see it
const response2 = await fakeRequest("/api/v1/custom_emojis", { await using client2 = await generateClient(users[0]);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
method: "GET",
});
expect(response2.ok).toBe(true); const { data: data2, ok: ok2 } =
const emojis = await response2.json(); await client2.getInstanceCustomEmojis();
expect(emojis).not.toContainEqual(expect.objectContaining({ id }));
expect(ok2).toBe(true);
expect(data2).not.toContainEqual(expect.objectContaining({ id }));
}); });
test("should delete the emoji", async () => { test("should delete the emoji", async () => {
const response = await fakeRequest(`/api/v1/emojis/${id}`, { await using client = await generateClient(users[1]);
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
method: "DELETE",
});
expect(response.status).toBe(204); const { ok } = await client.deleteEmoji(id);
expect(ok).toBe(true);
}); });
}); });

View file

@ -3,9 +3,9 @@ import { db } from "@versia/kit/db";
import { Emojis } from "@versia/kit/tables"; import { Emojis } from "@versia/kit/tables";
import { inArray } from "drizzle-orm"; import { inArray } from "drizzle-orm";
import sharp from "sharp"; import sharp from "sharp";
import { fakeRequest, getTestUsers } from "~/tests/utils"; import { generateClient, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(3); const { users, deleteUsers } = await getTestUsers(3);
// Make user 2 an admin // Make user 2 an admin
beforeAll(async () => { beforeAll(async () => {
@ -39,146 +39,109 @@ const createImage = async (name: string): Promise<File> => {
describe("/api/v1/emojis", () => { describe("/api/v1/emojis", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest("/api/v1/emojis", { await using client = await generateClient();
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
shortcode: "test",
element: "https://cdn.versia.social/logo.webp",
}),
});
expect(response.status).toBe(401); const { ok, raw } = await client.uploadEmoji(
"test",
new URL("https://cdn.versia.social/logo.webp"),
);
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
describe("Admin tests", () => { describe("Admin tests", () => {
test("should upload a file and create an emoji", async () => { test("should upload a file and create an emoji", async () => {
const formData = new FormData(); await using client = await generateClient(users[1]);
formData.append("shortcode", "test1");
formData.append("element", await createImage("test.png"));
formData.append("global", "true");
const response = await fakeRequest("/api/v1/emojis", { const { data, ok } = await client.uploadEmoji(
method: "POST", "test1",
headers: { await createImage("test.png"),
Authorization: `Bearer ${tokens[1].data.accessToken}`, {
global: true,
}, },
body: formData, );
});
expect(response.ok).toBe(true); expect(ok).toBe(true);
const emoji = await response.json(); expect(data.shortcode).toBe("test1");
expect(emoji.shortcode).toBe("test1"); expect(data.url).toContain("/media/proxy");
expect(emoji.url).toContain("/media/proxy");
}); });
test("should try to upload a non-image", async () => { test("should try to upload a non-image", async () => {
const formData = new FormData(); await using client = await generateClient(users[1]);
formData.append("shortcode", "test2");
formData.append("element", new File(["test"], "test.txt"));
const response = await fakeRequest("/api/v1/emojis", { const { ok, raw } = await client.uploadEmoji(
method: "POST", "test2",
headers: { new File(["test"], "test.txt"),
Authorization: `Bearer ${tokens[1].data.accessToken}`, );
},
body: formData,
});
expect(response.status).toBe(422); expect(ok).toBe(false);
expect(raw.status).toBe(422);
}); });
test("should upload an emoji by url", async () => { test("should upload an emoji by url", async () => {
const response = await fakeRequest("/api/v1/emojis", { await using client = await generateClient(users[1]);
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
shortcode: "test3",
element: "https://cdn.versia.social/logo.webp",
}),
});
expect(response.ok).toBe(true); const { data, ok } = await client.uploadEmoji(
const emoji = await response.json(); "test3",
expect(emoji.shortcode).toBe("test3"); new URL("https://cdn.versia.social/logo.webp"),
expect(emoji.url).toContain("/media/proxy/"); );
expect(ok).toBe(true);
expect(data.shortcode).toBe("test3");
expect(data.url).toContain("/media/proxy");
}); });
test("should fail when uploading an already existing emoji", async () => { test("should fail when uploading an already existing emoji", async () => {
const formData = new FormData(); await using client = await generateClient(users[1]);
formData.append("shortcode", "test1");
formData.append("element", await createImage("test-image.png"));
const response = await fakeRequest("/api/v1/emojis", { const { ok, raw } = await client.uploadEmoji(
method: "POST", "test1",
headers: { await createImage("test-image.png"),
Authorization: `Bearer ${tokens[1].data.accessToken}`, );
},
body: formData,
});
expect(response.status).toBe(422); expect(ok).toBe(false);
expect(raw.status).toBe(422);
}); });
}); });
describe("User tests", () => { describe("User tests", () => {
test("should upload a file and create an emoji", async () => { test("should upload a file and create an emoji", async () => {
const formData = new FormData(); await using client = await generateClient(users[0]);
formData.append("shortcode", "test4");
formData.append("element", await createImage("test-image.png"));
const response = await fakeRequest("/api/v1/emojis", { const { data, ok } = await client.uploadEmoji(
method: "POST", "test4",
headers: { await createImage("test-image.png"),
Authorization: `Bearer ${tokens[0].data.accessToken}`, );
},
body: formData,
});
expect(response.ok).toBe(true); expect(ok).toBe(true);
const emoji = await response.json(); expect(data.shortcode).toBe("test4");
expect(emoji.shortcode).toBe("test4"); expect(data.url).toContain("/media/proxy");
expect(emoji.url).toContain("/media/proxy/");
}); });
test("should fail when uploading an already existing global emoji", async () => { test("should fail when uploading an already existing global emoji", async () => {
const formData = new FormData(); await using client = await generateClient(users[0]);
formData.append("shortcode", "test1");
formData.append("element", await createImage("test-image.png"));
const response = await fakeRequest("/api/v1/emojis", { const { ok, raw } = await client.uploadEmoji(
method: "POST", "test1",
headers: { await createImage("test-image.png"),
Authorization: `Bearer ${tokens[0].data.accessToken}`, );
},
body: formData,
});
expect(response.status).toBe(422); expect(ok).toBe(false);
expect(raw.status).toBe(422);
}); });
test("should create an emoji as another user with the same shortcode", async () => { test("should create an emoji as another user with the same shortcode", async () => {
const formData = new FormData(); await using client = await generateClient(users[2]);
formData.append("shortcode", "test4");
formData.append("element", await createImage("test-image.png"));
const response = await fakeRequest("/api/v1/emojis", { const { data, ok } = await client.uploadEmoji(
method: "POST", "test4",
headers: { await createImage("test-image.png"),
Authorization: `Bearer ${tokens[2].data.accessToken}`, );
},
body: formData,
});
expect(response.ok).toBe(true); expect(ok).toBe(true);
const emoji = await response.json(); expect(data.shortcode).toBe("test4");
expect(emoji.shortcode).toBe("test4"); expect(data.url).toContain("/media/proxy/");
expect(emoji.url).toContain("/media/proxy/");
}); });
}); });
}); });

View file

@ -1,17 +1,15 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { fakeRequest } from "~/tests/utils"; import { generateClient } from "~/tests/utils";
// /api/v1/instance/extended_description // /api/v1/instance/extended_description
describe("/api/v1/instance/extended_description", () => { describe("/api/v1/instance/extended_description", () => {
test("should return extended description", async () => { test("should return extended description", async () => {
const response = await fakeRequest( await using client = await generateClient();
"/api/v1/instance/extended_description",
);
expect(response.status).toBe(200); const { data, ok } = await client.getInstanceExtendedDescription();
const json = await response.json(); expect(ok).toBe(true);
expect(json).toEqual({ expect(data).toEqual({
updated_at: new Date(0).toISOString(), updated_at: new Date(0).toISOString(),
content: content:
'<p>This is a <a href="https://versia.pub">Versia</a> server with the default extended description.</p>\n', '<p>This is a <a href="https://versia.pub">Versia</a> server with the default extended description.</p>\n',

View file

@ -1,15 +1,15 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { fakeRequest } from "~/tests/utils"; import { generateClient } from "~/tests/utils";
// /api/v1/instance/privacy_policy // /api/v1/instance/privacy_policy
describe("/api/v1/instance/privacy_policy", () => { describe("/api/v1/instance/privacy_policy", () => {
test("should return privacy policy", async () => { test("should return privacy policy", async () => {
const response = await fakeRequest("/api/v1/instance/privacy_policy"); await using client = await generateClient();
expect(response.status).toBe(200); const { data, ok } = await client.getInstancePrivacyPolicy();
const json = await response.json(); expect(ok).toBe(true);
expect(json).toEqual({ expect(data).toEqual({
updated_at: new Date(0).toISOString(), updated_at: new Date(0).toISOString(),
// This instance has not provided any privacy policy. // This instance has not provided any privacy policy.
content: content:

View file

@ -1,16 +1,16 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { config } from "~/config.ts"; import { config } from "~/config.ts";
import { fakeRequest } from "~/tests/utils"; import { generateClient } from "~/tests/utils";
// /api/v1/instance/rules // /api/v1/instance/rules
describe("/api/v1/instance/rules", () => { describe("/api/v1/instance/rules", () => {
test("should return rules", async () => { test("should return rules", async () => {
const response = await fakeRequest("/api/v1/instance/rules"); await using client = await generateClient();
expect(response.status).toBe(200); const { data, ok } = await client.getRules();
const json = await response.json(); expect(ok).toBe(true);
expect(json).toEqual( expect(data).toEqual(
config.instance.rules.map((r, index) => ({ config.instance.rules.map((r, index) => ({
id: String(index), id: String(index),
text: r.text, text: r.text,

View file

@ -1,15 +1,15 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { fakeRequest } from "~/tests/utils"; import { generateClient } from "~/tests/utils";
// /api/v1/instance/terms_of_service // /api/v1/instance/terms_of_service
describe("/api/v1/instance/terms_of_service", () => { describe("/api/v1/instance/terms_of_service", () => {
test("should return terms of service", async () => { test("should return terms of service", async () => {
const response = await fakeRequest("/api/v1/instance/terms_of_service"); await using client = await generateClient();
expect(response.status).toBe(200); const { data, ok } = await client.getInstanceTermsOfService();
const json = await response.json(); expect(ok).toBe(true);
expect(json).toEqual({ expect(data).toEqual({
updated_at: new Date(0).toISOString(), updated_at: new Date(0).toISOString(),
// This instance has not provided any terms of service. // This instance has not provided any terms of service.
content: content:

View file

@ -1,7 +1,7 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils"; import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(1); const { users, deleteUsers } = await getTestUsers(1);
const timeline = await getTestStatuses(10, users[0]); const timeline = await getTestStatuses(10, users[0]);
afterAll(async () => { afterAll(async () => {
@ -11,46 +11,34 @@ afterAll(async () => {
// /api/v1/markers // /api/v1/markers
describe("/api/v1/markers", () => { describe("/api/v1/markers", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest("/api/v1/markers", { await using client = await generateClient();
method: "GET",
}); const { ok, raw } = await client.getMarkers(["home", "notifications"]);
expect(response.status).toBe(401);
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should return empty markers", async () => { test("should return empty markers", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/markers?${new URLSearchParams([
["timeline[]", "home"],
["timeline[]", "notifications"],
])}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200); const { data, ok } = await client.getMarkers(["home", "notifications"]);
expect(await response.json()).toEqual({});
expect(ok).toBe(true);
expect(data).toEqual({});
}); });
test("should create markers", async () => { test("should create markers", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/markers?${new URLSearchParams({
"home[last_read_id]": timeline[0].id,
})}`,
{ const { data, ok } = await client.saveMarkers({
method: "POST", home: {
headers: { last_read_id: timeline[0].id,
Authorization: `Bearer ${tokens[0].data.accessToken}`,
}, },
}, });
);
expect(response.status).toBe(200); expect(ok).toBe(true);
expect(await response.json()).toEqual({ expect(data).toEqual({
home: { home: {
last_read_id: timeline[0].id, last_read_id: timeline[0].id,
updated_at: expect.any(String), updated_at: expect.any(String),
@ -60,21 +48,12 @@ describe("/api/v1/markers", () => {
}); });
test("should return markers", async () => { test("should return markers", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/markers?${new URLSearchParams([
["timeline[]", "home"],
["timeline[]", "notifications"],
])}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200); const { data, ok } = await client.getMarkers(["home", "notifications"]);
expect(await response.json()).toEqual({
expect(ok).toBe(true);
expect(data).toEqual({
home: { home: {
last_read_id: timeline[0].id, last_read_id: timeline[0].id,
updated_at: expect.any(String), updated_at: expect.any(String),

View file

@ -1,76 +1,54 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { fakeRequest, getTestUsers } from "~/tests/utils"; import { generateClient, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(3); const { users, deleteUsers } = await getTestUsers(3);
afterAll(async () => { afterAll(async () => {
await deleteUsers(); await deleteUsers();
}); });
beforeAll(async () => { beforeAll(async () => {
const response = await fakeRequest(`/api/v1/accounts/${users[1].id}/mute`, { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { ok } = await client.muteAccount(users[1].id);
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json", expect(ok).toBe(true);
},
body: JSON.stringify({}),
});
expect(response.status).toBe(200);
}); });
// /api/v1/mutes // /api/v1/mutes
describe("/api/v1/mutes", () => { describe("/api/v1/mutes", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest( await using client = await generateClient();
"/api/v1/mutes".replace(":id", users[1].id),
{ const { ok, raw } = await client.getMutes();
method: "GET",
}, expect(ok).toBe(false);
); expect(raw.status).toBe(401);
expect(response.status).toBe(401);
}); });
test("should return mutes", async () => { test("should return mutes", async () => {
const response = await fakeRequest("/api/v1/mutes", { await using client = await generateClient(users[0]);
method: "GET",
headers: { const { data, ok } = await client.getMutes();
Authorization: `Bearer ${tokens[0].data.accessToken}`,
}, expect(ok).toBe(true);
}); expect(data).toEqual([
expect(response.status).toBe(200);
const body = await response.json();
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: users[1].id, id: users[1].id,
}), }),
]), ]);
);
}); });
test("should return mutes after unmute", async () => { test("should return mutes after unmute", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/accounts/${users[1].id}/unmute`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
expect(response.status).toBe(200);
const response2 = await fakeRequest("/api/v1/mutes", { const { ok } = await client.unmuteAccount(users[1].id);
method: "GET",
headers: { expect(ok).toBe(true);
Authorization: `Bearer ${tokens[0].data.accessToken}`,
}, const { data, ok: ok2 } = await client.getMutes();
});
expect(response2.status).toBe(200); expect(ok2).toBe(true);
const body = await response2.json(); expect(data).toEqual([]);
expect(body).toEqual([]);
}); });
}); });

View file

@ -1,28 +1,24 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Notification as ApiNotification } from "@versia/client/types"; import type { z } from "@hono/zod-openapi";
import { fakeRequest, getTestUsers } from "~/tests/utils"; import type { Notification } from "@versia/client-ng/schemas";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, deleteUsers } = await getTestUsers(2);
let notifications: ApiNotification[] = []; let notifications: z.infer<typeof Notification>[] = [];
// Create some test notifications: follow, favourite, reblog, mention // Create some test notifications: follow, favourite, reblog, mention
beforeAll(async () => { beforeAll(async () => {
await fakeRequest(`/api/v1/accounts/${users[0].id}/follow`, { await using client0 = await generateClient(users[0]);
method: "POST", await using client1 = await generateClient(users[1]);
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
notifications = await fakeRequest("/api/v1/notifications", { const { ok } = await client1.followAccount(users[0].id);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
}).then((r) => r.json());
expect(notifications.length).toBe(1); expect(ok).toBe(true);
const { data } = await client0.getNotifications();
expect(data).toBeArrayOfSize(1);
notifications = data;
}); });
afterAll(async () => { afterAll(async () => {
@ -31,55 +27,41 @@ afterAll(async () => {
describe("/api/v1/notifications/:id/dismiss", () => { describe("/api/v1/notifications/:id/dismiss", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest( await using client = await generateClient();
`/api/v1/notifications/${notifications[0].id}/dismiss`,
{ const { ok, raw } = await client.dismissNotification(
method: "POST", notifications[0].id,
},
); );
expect(response.status).toBe(401); expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should dismiss notification", async () => { test("should dismiss notification", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/notifications/${notifications[0].id}/dismiss`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200); const { ok } = await client.dismissNotification(notifications[0].id);
expect(ok).toBe(true);
}); });
test("should not display dismissed notification", async () => { test("should not display dismissed notification", async () => {
const response = await fakeRequest("/api/v1/notifications", { await using client = await generateClient(users[0]);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.status).toBe(200); const { data, ok } = await client.getNotifications();
const output = await response.json(); expect(ok).toBe(true);
expect(data).toBeArrayOfSize(0);
expect(output.length).toBe(0);
}); });
test("should not be able to dismiss other user's notifications", async () => { test("should not be able to dismiss other user's notifications", async () => {
const response = await fakeRequest( await using client = await generateClient(users[1]);
`/api/v1/notifications/${notifications[0].id}/dismiss`,
{ const { ok, raw } = await client.dismissNotification(
method: "POST", notifications[0].id,
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
},
); );
expect(response.status).toBe(404); expect(ok).toBe(false);
expect(raw.status).toBe(404);
}); });
}); });

View file

@ -1,28 +1,23 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Notification as ApiNotification } from "@versia/client/types"; import type { z } from "@hono/zod-openapi";
import { fakeRequest, getTestUsers } from "~/tests/utils"; import type { Notification } from "@versia/client-ng/schemas";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, deleteUsers } = await getTestUsers(2);
let notifications: ApiNotification[] = []; let notifications: z.infer<typeof Notification>[] = [];
// Create some test notifications: follow, favourite, reblog, mention
beforeAll(async () => { beforeAll(async () => {
await fakeRequest(`/api/v1/accounts/${users[0].id}/follow`, { await using client0 = await generateClient(users[0]);
method: "POST", await using client1 = await generateClient(users[1]);
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
notifications = await fakeRequest("/api/v1/notifications", { const { ok } = await client1.followAccount(users[0].id);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
}).then((r) => r.json());
expect(notifications.length).toBe(1); expect(ok).toBe(true);
const { data } = await client0.getNotifications();
expect(data).toBeArrayOfSize(1);
notifications = data;
}); });
afterAll(async () => { afterAll(async () => {
@ -32,66 +27,54 @@ afterAll(async () => {
// /api/v1/notifications/:id // /api/v1/notifications/:id
describe("/api/v1/notifications/:id", () => { describe("/api/v1/notifications/:id", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest( await using client = await generateClient();
"/api/v1/notifications/00000000-0000-0000-0000-000000000000",
);
expect(response.status).toBe(401); const { ok, raw } = await client.getNotification(notifications[0].id);
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should return 422 if ID is invalid", async () => { test("should return 422 if ID is invalid", async () => {
const response = await fakeRequest("/api/v1/notifications/invalid", { await using client = await generateClient(users[0]);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`, const { ok, raw } = await client.getNotification("invalid");
},
}); expect(ok).toBe(false);
expect(response.status).toBe(422); expect(raw.status).toBe(422);
}); });
test("should return 404 if notification not found", async () => { test("should return 404 if notification not found", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
"/api/v1/notifications/00000000-0000-0000-0000-000000000000",
{ const { ok, raw } = await client.getNotification(
headers: { "00000000-0000-0000-0000-000000000000",
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
); );
expect(response.status).toBe(404); expect(ok).toBe(false);
expect(raw.status).toBe(404);
}); });
test("should return notification", async () => { test("should return notification", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/notifications/${notifications[0].id}`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200); const { ok, data } = await client.getNotification(notifications[0].id);
const notification = (await response.json()) as ApiNotification; expect(ok).toBe(true);
expect(notification).toBeDefined(); expect(data).toBeDefined();
expect(notification.id).toBe(notifications[0].id); expect(data.id).toBe(notifications[0].id);
expect(notification.type).toBe("follow"); expect(data.type).toBe("follow");
expect(notification.account).toBeDefined(); expect(data.account).toBeDefined();
expect(notification.account?.id).toBe(users[1].id); expect(data.account?.id).toBe(users[1].id);
}); });
test("should not be able to view other user's notifications", async () => { test("should not be able to view other user's notifications", async () => {
const response = await fakeRequest( await using client = await generateClient(users[1]);
`/api/v1/notifications/${notifications[0].id}`,
{
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
},
);
expect(response.status).toBe(404); const { ok, raw } = await client.getNotification(notifications[0].id);
expect(ok).toBe(false);
expect(raw.status).toBe(404);
}); });
}); });

View file

@ -1,28 +1,21 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Notification as ApiNotification } from "@versia/client/types"; import { generateClient, getTestUsers } from "~/tests/utils";
import { fakeRequest, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, deleteUsers } = await getTestUsers(2);
let notifications: ApiNotification[] = [];
// Create some test notifications: follow, favourite, reblog, mention // Create some test notifications: follow, favourite, reblog, mention
beforeAll(async () => { beforeAll(async () => {
await fakeRequest(`/api/v1/accounts/${users[0].id}/follow`, { await using client1 = await generateClient(users[1]);
method: "POST", await using client0 = await generateClient(users[0]);
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
notifications = await fakeRequest("/api/v1/notifications", { const { ok } = await client1.followAccount(users[0].id);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
}).then((r) => r.json());
expect(notifications.length).toBe(1); expect(ok).toBe(true);
const { data, ok: ok2 } = await client0.getNotifications();
expect(ok2).toBe(true);
expect(data).toBeArrayOfSize(1);
}); });
afterAll(async () => { afterAll(async () => {
@ -32,29 +25,23 @@ afterAll(async () => {
// /api/v1/notifications/clear // /api/v1/notifications/clear
describe("/api/v1/notifications/clear", () => { describe("/api/v1/notifications/clear", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest("/api/v1/notifications/clear", { await using client = await generateClient();
method: "POST",
});
expect(response.status).toBe(401); const { ok, raw } = await client.dismissNotifications();
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should clear notifications", async () => { test("should clear notifications", async () => {
const response = await fakeRequest("/api/v1/notifications/clear", { await using client = await generateClient(users[0]);
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.status).toBe(200); const { ok } = await client.dismissNotifications();
const newNotifications = await fakeRequest("/api/v1/notifications", { expect(ok).toBe(true);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
}).then((r) => r.json());
expect(newNotifications.length).toBe(0); const { data: newNotifications } = await client.getNotifications();
expect(newNotifications).toBeArrayOfSize(0);
}); });
}); });

View file

@ -1,40 +1,29 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Notification as ApiNotification } from "@versia/client/types"; import type { z } from "@hono/zod-openapi";
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils"; import type { Notification } from "@versia/client-ng/schemas";
import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, deleteUsers } = await getTestUsers(2);
const statuses = await getTestStatuses(40, users[0]); const statuses = await getTestStatuses(5, users[0]);
let notifications: ApiNotification[] = []; let notifications: z.infer<typeof Notification>[] = [];
// Create some test notifications // Create some test notifications
beforeAll(async () => { beforeAll(async () => {
await fakeRequest(`/api/v1/accounts/${users[0].id}/follow`, { await using client0 = await generateClient(users[0]);
method: "POST", await using client1 = await generateClient(users[1]);
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`, const { ok } = await client1.followAccount(users[0].id);
"Content-Type": "application/json",
}, expect(ok).toBe(true);
body: JSON.stringify({}),
});
for (const i of [0, 1, 2, 3]) { for (const i of [0, 1, 2, 3]) {
await fakeRequest(`/api/v1/statuses/${statuses[i].id}/favourite`, { await client1.favouriteStatus(statuses[i].id);
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
} }
notifications = await fakeRequest("/api/v1/notifications", { const { data } = await client0.getNotifications();
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
}).then((r) => r.json());
expect(notifications.length).toBe(5); expect(data).toBeArrayOfSize(5);
notifications = data;
}); });
afterAll(async () => { afterAll(async () => {
@ -44,45 +33,33 @@ afterAll(async () => {
// /api/v1/notifications/destroy_multiple // /api/v1/notifications/destroy_multiple
describe("/api/v1/notifications/destroy_multiple", () => { describe("/api/v1/notifications/destroy_multiple", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest( await using client = await generateClient();
`/api/v1/notifications/destroy_multiple?${new URLSearchParams(
notifications.slice(1).map((n) => ["ids[]", n.id]), const { ok, raw } = await client.dismissMultipleNotifications(
).toString()}`, notifications.slice(1).map((n) => n.id),
{
method: "DELETE",
},
); );
expect(response.status).toBe(401); expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should dismiss notifications", async () => { test("should dismiss notifications", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/notifications/destroy_multiple?${new URLSearchParams(
notifications.slice(1).map((n) => ["ids[]", n.id]), const { ok, raw } = await client.dismissMultipleNotifications(
).toString()}`, notifications.slice(1).map((n) => n.id),
{
method: "DELETE",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
); );
expect(response.status).toBe(200); expect(ok).toBe(true);
expect(raw.status).toBe(200);
}); });
test("should not display dismissed notification", async () => { test("should not display dismissed notifications", async () => {
const response = await fakeRequest("/api/v1/notifications", { await using client = await generateClient(users[0]);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.status).toBe(200); const { data } = await client.getNotifications();
const output = await response.json(); expect(data).toBeArrayOfSize(1);
expect(data[0].id).toBe(notifications[0].id);
expect(output.length).toBe(1);
}); });
}); });

View file

@ -1,70 +1,33 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Notification as ApiNotification } from "@versia/client/types"; import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils";
const getFormData = ( const { users, deleteUsers } = await getTestUsers(2);
object: Record<string, string | number | boolean>, const timeline = (await getTestStatuses(5, users[0])).toReversed();
): FormData =>
Object.keys(object).reduce((formData, key) => {
formData.append(key, String(object[key]));
return formData;
}, new FormData());
const { users, tokens, deleteUsers } = await getTestUsers(2);
const timeline = (await getTestStatuses(40, users[0])).toReversed();
// Create some test notifications: follow, favourite, reblog, mention // Create some test notifications: follow, favourite, reblog, mention
beforeAll(async () => { beforeAll(async () => {
const res1 = await fakeRequest(`/api/v1/accounts/${users[0].id}/follow`, { await using client = await generateClient(users[1]);
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
expect(res1.status).toBe(200); const { ok } = await client.followAccount(users[0].id);
const res2 = await fakeRequest( expect(ok).toBe(true);
`/api/v1/statuses/${timeline[0].id}/favourite`,
const { ok: ok2 } = await client.favouriteStatus(timeline[0].id);
expect(ok2).toBe(true);
const { ok: ok3 } = await client.reblogStatus(timeline[0].id);
expect(ok3).toBe(true);
const { ok: ok4 } = await client.postStatus(
`@${users[0].data.username} test mention`,
{ {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
expect(res2.status).toBe(200);
const res3 = await fakeRequest(
`/api/v1/statuses/${timeline[0].id}/reblog`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
body: getFormData({}),
},
);
expect(res3.status).toBe(200);
const res4 = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
body: new URLSearchParams({
status: `@${users[0].data.username} test mention`,
visibility: "direct", visibility: "direct",
local_only: "true", local_only: true,
}), },
}); );
expect(res4.status).toBe(200); expect(ok4).toBe(true);
}); });
afterAll(async () => { afterAll(async () => {
@ -74,27 +37,22 @@ afterAll(async () => {
// /api/v1/notifications // /api/v1/notifications
describe("/api/v1/notifications", () => { describe("/api/v1/notifications", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest("/api/v1/notifications"); await using client = await generateClient();
expect(response.status).toBe(401); const { ok, raw } = await client.getNotifications();
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should return 200 with notifications", async () => { test("should return 200 with notifications", async () => {
const response = await fakeRequest("/api/v1/notifications", { await using client = await generateClient(users[0]);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.status).toBe(200); const { data, ok } = await client.getNotifications();
expect(response.headers.get("content-type")).toContain(
"application/json",
);
const objects = (await response.json()) as ApiNotification[]; expect(ok).toBe(true);
expect(data.length).toBe(4);
expect(objects.length).toBe(4); for (const [index, notification] of data.entries()) {
for (const [index, notification] of objects.entries()) {
expect(notification.account).toBeDefined(); expect(notification.account).toBeDefined();
expect(notification.account?.id).toBe(users[1].id); expect(notification.account?.id).toBe(users[1].id);
expect(notification.created_at).toBeDefined(); expect(notification.created_at).toBeDefined();
@ -103,64 +61,43 @@ describe("/api/v1/notifications", () => {
expect(notification.type).toBe( expect(notification.type).toBe(
["follow", "favourite", "reblog", "mention"].toReversed()[ ["follow", "favourite", "reblog", "mention"].toReversed()[
index index
], ] as "follow" | "favourite" | "reblog" | "mention",
); );
} }
}); });
test("should not return notifications with filtered keywords", async () => { test("should not return notifications with filtered keywords", async () => {
const filterResponse = await fakeRequest("/api/v2/filters", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { data: filter, ok } = await client.createFilter(
Authorization: `Bearer ${tokens[0].data.accessToken}`, ["notifications"],
"Content-Type": "application/x-www-form-urlencoded", "Test Filter",
"hide",
{
keywords_attributes: [
{
keyword: timeline[0].content.slice(4, 20),
whole_word: false,
}, },
body: new URLSearchParams({ ],
title: "Test Filter",
"context[]": "notifications",
filter_action: "hide",
"keywords_attributes[0][keyword]": timeline[0].content.slice(
4,
20,
),
"keywords_attributes[0][whole_word]": "false",
}),
});
expect(filterResponse.status).toBe(200);
const response = await fakeRequest("/api/v1/notifications?limit=20", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
}, },
});
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
); );
const objects = (await response.json()) as ApiNotification[]; expect(ok).toBe(true);
expect(objects.length).toBe(3); const { data: notifications } = await client.getNotifications();
expect(notifications.length).toBe(3);
// There should be no element with a status with id of timeline[0].id // There should be no element with a status with id of timeline[0].id
expect(objects).not.toContainEqual( expect(notifications).not.toContainEqual(
expect.objectContaining({ expect.objectContaining({
status: expect.objectContaining({ id: timeline[0].id }), status: expect.objectContaining({ id: timeline[0].id }),
}), }),
); );
// Delete filter // Delete filter
const filterDeleteResponse = await fakeRequest( const { ok: ok2 } = await client.deleteFilter(filter.id);
`/api/v2/filters/${(await filterResponse.json()).id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(filterDeleteResponse.status).toBe(204); expect(ok2).toBe(true);
}); });
}); });

View file

@ -1,6 +1,6 @@
import { afterAll, beforeEach, describe, expect, test } from "bun:test"; import { afterAll, beforeEach, describe, expect, test } from "bun:test";
import { PushSubscription } from "@versia/kit/db"; import { PushSubscription } from "@versia/kit/db";
import { fakeRequest, getTestUsers } from "~/tests/utils"; import { generateClient, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, tokens, deleteUsers } = await getTestUsers(2);
@ -15,42 +15,34 @@ beforeEach(async () => {
describe("/api/v1/push/subscriptions", () => { describe("/api/v1/push/subscriptions", () => {
test("should create a push subscription", async () => { test("should create a push subscription", async () => {
const res = await fakeRequest("/api/v1/push/subscription", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { ok, data } = await client.subscribePushNotifications(
Authorization: `Bearer ${tokens[0].data.accessToken}`, {
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
alerts: {
update: true,
},
},
policy: "all",
subscription: {
endpoint: "https://example.com", endpoint: "https://example.com",
keys: { keys: {
auth: "test",
p256dh: "test", p256dh: "test",
auth: "testthatis24charactersha",
}, },
}, },
}), {
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body).toMatchObject({
endpoint: "https://example.com",
alerts: { alerts: {
update: true, update: true,
}, },
policy: "all",
},
);
expect(ok).toBe(true);
expect(data.endpoint).toBe("https://example.com");
expect(data.alerts).toMatchObject({
update: true,
}); });
}); });
test("should retrieve the same push subscription", async () => { test("should retrieve the same push subscription", async () => {
await using client = await generateClient(users[1]);
await PushSubscription.insert({ await PushSubscription.insert({
endpoint: "https://example.com", endpoint: "https://example.com",
alerts: { alerts: {
@ -68,28 +60,21 @@ describe("/api/v1/push/subscriptions", () => {
policy: "all", policy: "all",
authSecret: "test", authSecret: "test",
publicKey: "test", publicKey: "test",
tokenId: tokens[1].id, tokenId: client.dbToken.id,
}); });
const res = await fakeRequest("/api/v1/push/subscription", { const { ok, data } = await client.getPushSubscription();
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
});
expect(res.status).toBe(200); expect(ok).toBe(true);
expect(data.endpoint).toBe("https://example.com");
const body = await res.json(); expect(data.alerts).toMatchObject({
expect(body).toMatchObject({
endpoint: "https://example.com",
alerts: {
update: true, update: true,
},
}); });
}); });
test("should update a push subscription", async () => { test("should update a push subscription", async () => {
await using client = await generateClient(users[0]);
await PushSubscription.insert({ await PushSubscription.insert({
endpoint: "https://example.com", endpoint: "https://example.com",
alerts: { alerts: {
@ -107,65 +92,49 @@ describe("/api/v1/push/subscriptions", () => {
policy: "all", policy: "all",
authSecret: "test", authSecret: "test",
publicKey: "test", publicKey: "test",
tokenId: tokens[0].id, tokenId: client.dbToken.id,
}); });
const res = await fakeRequest("/api/v1/push/subscription", { const { ok, data } = await client.updatePushSubscription(
method: "PUT", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
alerts: { alerts: {
update: false, update: false,
favourite: true, favourite: true,
}, },
}, },
policy: "follower", "follower",
}), );
});
expect(res.status).toBe(200); expect(ok).toBe(true);
expect(await res.json()).toMatchObject({ expect(data.alerts).toMatchObject({
alerts: {
update: false, update: false,
favourite: true, favourite: true,
},
}); });
}); });
describe("permissions", () => { describe("permissions", () => {
test("should not allow watching admin reports without permissions", async () => { test("should not allow watching admin reports without permissions", async () => {
const res = await fakeRequest("/api/v1/push/subscription", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { data, ok } = await client.subscribePushNotifications(
Authorization: `Bearer ${tokens[0].data.accessToken}`, {
"Content-Type": "application/json", endpoint: "https://example.com",
keys: {
auth: "testthatis24charactersha",
p256dh: "test",
}, },
body: JSON.stringify({ },
data: { {
alerts: { alerts: {
"admin.report": true, "admin.report": true,
}, },
},
policy: "all", policy: "all",
subscription: {
endpoint: "https://example.com",
keys: {
p256dh: "test",
auth: "testthatis24charactersha",
}, },
}, );
}),
});
expect(res.status).toBe(200); expect(ok).toBe(true);
expect(await res.json()).toMatchObject({ expect(data.alerts).toMatchObject({
alerts: {
"admin.report": false, "admin.report": false,
},
}); });
}); });
@ -174,30 +143,28 @@ describe("/api/v1/push/subscriptions", () => {
isAdmin: true, isAdmin: true,
}); });
const res = await fakeRequest("/api/v1/push/subscription", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { ok, data } = await client.subscribePushNotifications(
Authorization: `Bearer ${tokens[0].data.accessToken}`, {
"Content-Type": "application/json", endpoint: "https://example.com",
keys: {
auth: "testthatis24charactersha",
p256dh: "test",
}, },
body: JSON.stringify({ },
data: { {
alerts: { alerts: {
"admin.report": true, "admin.report": true,
}, },
},
policy: "all", policy: "all",
subscription: {
endpoint: "https://example.com",
keys: {
p256dh: "test",
auth: "testthatis24charactersha",
}, },
}, );
}),
});
expect(res.status).toBe(200); expect(ok).toBe(true);
expect(data.alerts).toMatchObject({
"admin.report": true,
});
await users[0].update({ await users[0].update({
isAdmin: false, isAdmin: false,
@ -205,6 +172,8 @@ describe("/api/v1/push/subscriptions", () => {
}); });
test("should not allow editing to add admin reports without permissions", async () => { test("should not allow editing to add admin reports without permissions", async () => {
await using client = await generateClient(users[0]);
await PushSubscription.insert({ await PushSubscription.insert({
endpoint: "https://example.com", endpoint: "https://example.com",
alerts: { alerts: {
@ -222,30 +191,21 @@ describe("/api/v1/push/subscriptions", () => {
policy: "all", policy: "all",
authSecret: "test", authSecret: "test",
publicKey: "test", publicKey: "test",
tokenId: tokens[0].id, tokenId: client.dbToken.id,
}); });
const res = await fakeRequest("/api/v1/push/subscription", { const { ok, data } = await client.updatePushSubscription(
method: "PUT", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
alerts: { alerts: {
"admin.report": true, "admin.report": true,
}, },
}, },
policy: "all", "all",
}), );
});
expect(res.status).toBe(200); expect(ok).toBe(true);
expect(await res.json()).toMatchObject({ expect(data.alerts).toMatchObject({
alerts: {
"admin.report": false, "admin.report": false,
},
}); });
}); });
@ -254,6 +214,8 @@ describe("/api/v1/push/subscriptions", () => {
isAdmin: true, isAdmin: true,
}); });
await using client = await generateClient(users[0]);
await PushSubscription.insert({ await PushSubscription.insert({
endpoint: "https://example.com", endpoint: "https://example.com",
alerts: { alerts: {
@ -271,28 +233,20 @@ describe("/api/v1/push/subscriptions", () => {
policy: "all", policy: "all",
authSecret: "test", authSecret: "test",
publicKey: "test", publicKey: "test",
tokenId: tokens[0].id, tokenId: client.dbToken.id,
}); });
const res = await fakeRequest("/api/v1/push/subscription", { const { ok, data } = await client.updatePushSubscription(
method: "PUT", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
alerts: { alerts: {
"admin.report": true, "admin.report": true,
}, },
}, },
policy: "all", "all",
}), );
});
expect(res.status).toBe(200); expect(ok).toBe(true);
expect(await res.json()).toMatchObject({ expect(data.alerts).toMatchObject({
alerts: {
update: true, update: true,
"admin.report": true, "admin.report": true,
"admin.sign_up": false, "admin.sign_up": false,
@ -303,7 +257,6 @@ describe("/api/v1/push/subscriptions", () => {
poll: false, poll: false,
reblog: false, reblog: false,
status: false, status: false,
},
}); });
await users[0].update({ await users[0].update({

View file

@ -1,9 +1,9 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { Role } from "@versia/kit/db"; import { Role } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { fakeRequest, getTestUsers } from "~/tests/utils"; import { generateClient, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, deleteUsers } = await getTestUsers(2);
let role: Role; let role: Role;
let higherPriorityRole: Role; let higherPriorityRole: Role;
@ -44,38 +44,32 @@ afterAll(async () => {
// /api/v1/roles/:id // /api/v1/roles/:id
describe("/api/v1/roles/:id", () => { describe("/api/v1/roles/:id", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(`/api/v1/roles/${role.id}`, { await using client = await generateClient();
method: "GET",
});
expect(response.status).toBe(401); const { ok, raw } = await client.getRole(role.id);
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should return 404 if role does not exist", async () => { test("should return 404 if role does not exist", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
"/api/v1/roles/00000000-0000-0000-0000-000000000000",
{ const { ok, raw } = await client.getRole(
method: "GET", "00000000-0000-0000-0000-000000000000",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
); );
expect(response.status).toBe(404); expect(ok).toBe(false);
expect(raw.status).toBe(404);
}); });
test("should return role data", async () => { test("should return role data", async () => {
const response = await fakeRequest(`/api/v1/roles/${role.id}`, { await using client = await generateClient(users[0]);
method: "GET",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.status).toBe(200); const { ok, data } = await client.getRole(role.id);
const responseData = await response.json();
expect(responseData).toMatchObject({ expect(ok).toBe(true);
expect(data).toMatchObject({
id: role.id, id: role.id,
name: role.data.name, name: role.data.name,
permissions: role.data.permissions, permissions: role.data.permissions,
@ -87,63 +81,56 @@ describe("/api/v1/roles/:id", () => {
}); });
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(`/api/v1/roles/${role.id}`, { await using client = await generateClient();
method: "PATCH",
headers: { "Content-Type": "application/json" }, const { ok, raw } = await client.updateRole(role.id, {
body: JSON.stringify({ name: "updatedName" }), name: "updatedName",
}); });
expect(response.status).toBe(401); expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should return 404 if role does not exist", async () => { test("should return 404 if role does not exist", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
"/api/v1/roles/00000000-0000-0000-0000-000000000000",
const { ok, raw } = await client.updateRole(
"00000000-0000-0000-0000-000000000000",
{ {
method: "PATCH", name: "updatedName",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "updatedName" }),
}, },
); );
expect(response.status).toBe(404); expect(ok).toBe(false);
expect(raw.status).toBe(404);
}); });
test("should update role data", async () => { test("should update role data", async () => {
const response = await fakeRequest(`/api/v1/roles/${role.id}`, { await using client = await generateClient(users[0]);
method: "PATCH",
headers: { const { ok } = await client.updateRole(role.id, {
Authorization: `Bearer ${tokens[0].data.accessToken}`, name: "updatedName",
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "updatedName" }),
}); });
expect(response.status).toBe(204); expect(ok).toBe(true);
const updatedRole = await Role.fromId(role.id); const updatedRole = await Role.fromId(role.id);
expect(updatedRole?.data.name).toBe("updatedName"); expect(updatedRole?.data.name).toBe("updatedName");
}); });
test("should return 403 if user tries to update role with higher priority", async () => { test("should return 403 if user tries to update role with higher priority", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/roles/${higherPriorityRole.id}`,
const { data, ok, raw } = await client.updateRole(
higherPriorityRole.id,
{ {
method: "PATCH", name: "updatedName",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "updatedName" }),
}, },
); );
expect(response.status).toBe(403); expect(ok).toBe(false);
const output = await response.json(); expect(raw.status).toBe(403);
expect(output).toMatchObject({ expect(data).toMatchObject({
error: "Forbidden", error: "Forbidden",
details: details:
"User with highest role priority 2 cannot edit role with priority 3", "User with highest role priority 2 cannot edit role with priority 3",
@ -151,45 +138,38 @@ describe("/api/v1/roles/:id", () => {
}); });
test("should return 403 if user tries to update role with permissions they do not have", async () => { test("should return 403 if user tries to update role with permissions they do not have", async () => {
const response = await fakeRequest(`/api/v1/roles/${role.id}`, { await using client = await generateClient(users[0]);
method: "PATCH",
headers: { const { data, ok, raw } = await client.updateRole(role.id, {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
permissions: [RolePermissions.Impersonate], permissions: [RolePermissions.Impersonate],
}),
}); });
expect(response.status).toBe(403); expect(ok).toBe(false);
const output = await response.json(); expect(raw.status).toBe(403);
expect(output).toMatchObject({ expect(data).toMatchObject({
error: "Forbidden", error: "Forbidden",
details: "User cannot add or remove permissions they do not have", details: "User cannot add or remove permissions they do not have",
}); });
}); });
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(`/api/v1/roles/${role.id}`, { await using client = await generateClient();
method: "DELETE",
});
expect(response.status).toBe(401); const { ok, raw } = await client.deleteRole(role.id);
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should return 404 if role does not exist", async () => { test("should return 404 if role does not exist", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
"/api/v1/roles/00000000-0000-0000-0000-000000000000",
{ const { ok, raw } = await client.deleteRole(
method: "DELETE", "00000000-0000-0000-0000-000000000000",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
); );
expect(response.status).toBe(404); expect(ok).toBe(false);
expect(raw.status).toBe(404);
}); });
test("should delete role", async () => { test("should delete role", async () => {
@ -202,33 +182,26 @@ describe("/api/v1/roles/:id", () => {
icon: "test", icon: "test",
}); });
const response = await fakeRequest(`/api/v1/roles/${newRole.id}`, { await using client = await generateClient(users[0]);
method: "DELETE",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.status).toBe(204); const { ok } = await client.deleteRole(newRole.id);
expect(ok).toBe(true);
const deletedRole = await Role.fromId(newRole.id); const deletedRole = await Role.fromId(newRole.id);
expect(deletedRole).toBeNull(); expect(deletedRole).toBeNull();
}); });
test("should return 403 if user tries to delete role with higher priority", async () => { test("should return 403 if user tries to delete role with higher priority", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/roles/${higherPriorityRole.id}`,
{ const { data, ok, raw } = await client.deleteRole(
method: "DELETE", higherPriorityRole.id,
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
); );
expect(response.status).toBe(403); expect(ok).toBe(false);
const output = await response.json(); expect(raw.status).toBe(403);
expect(output).toMatchObject({ expect(data).toMatchObject({
error: "Forbidden", error: "Forbidden",
details: details:
"User with highest role priority 2 cannot delete role with priority 3", "User with highest role priority 2 cannot delete role with priority 3",

View file

@ -2,9 +2,9 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { Role } from "@versia/kit/db"; import { Role } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { config } from "~/config.ts"; import { config } from "~/config.ts";
import { fakeRequest, getTestUsers } from "~/tests/utils"; import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers, tokens } = await getTestUsers(1); const { users, deleteUsers } = await getTestUsers(1);
let role: Role; let role: Role;
beforeAll(async () => { beforeAll(async () => {
@ -32,24 +32,21 @@ afterAll(async () => {
// /api/v1/roles // /api/v1/roles
describe("/api/v1/roles", () => { describe("/api/v1/roles", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest("/api/v1/roles", { await using client = await generateClient();
method: "GET",
});
expect(response.status).toBe(401); const { ok, raw } = await client.getRoles();
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should return a list of roles", async () => { test("should return a list of roles", async () => {
const response = await fakeRequest("/api/v1/roles", { await using client = await generateClient(users[0]);
method: "GET",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.ok).toBe(true); const { ok, data } = await client.getRoles();
const roles = await response.json();
expect(roles).toContainEqual({ expect(ok).toBe(true);
expect(data).toContainEqual({
name: "test", name: "test",
permissions: [RolePermissions.ManageRoles], permissions: [RolePermissions.ManageRoles],
priority: 10, priority: 10,
@ -59,7 +56,7 @@ describe("/api/v1/roles", () => {
id: role.id, id: role.id,
}); });
expect(roles).toContainEqual({ expect(data).toContainEqual({
id: "default", id: "default",
name: "Default", name: "Default",
permissions: config.permissions.default, permissions: config.permissions.default,
@ -70,25 +67,18 @@ describe("/api/v1/roles", () => {
}); });
test("should create a new role", async () => { test("should create a new role", async () => {
const response = await fakeRequest("/api/v1/roles", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { ok, data } = await client.createRole("newRole", {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "newRole",
permissions: [RolePermissions.ManageRoles], permissions: [RolePermissions.ManageRoles],
priority: 1, priority: 1,
description: "newRole", description: "newRole",
visible: true, visible: true,
icon: "https://example.com/icon.png", icon: "https://example.com/icon.png",
}),
}); });
expect(response.ok).toBe(true); expect(ok).toBe(true);
const newRole = await response.json(); expect(data).toMatchObject({
expect(newRole).toMatchObject({
name: "newRole", name: "newRole",
permissions: [RolePermissions.ManageRoles], permissions: [RolePermissions.ManageRoles],
priority: 1, priority: 1,
@ -98,7 +88,7 @@ describe("/api/v1/roles", () => {
}); });
// Cleanup // Cleanup
const createdRole = await Role.fromId(newRole.id); const createdRole = await Role.fromId(data.id);
expect(createdRole).toBeDefined(); expect(createdRole).toBeDefined();
@ -106,49 +96,37 @@ describe("/api/v1/roles", () => {
}); });
test("should return 403 if user tries to create a role with higher priority", async () => { test("should return 403 if user tries to create a role with higher priority", async () => {
const response = await fakeRequest("/api/v1/roles", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { data, ok, raw } = await client.createRole("newRole", {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "newRole",
permissions: [RolePermissions.ManageBlocks], permissions: [RolePermissions.ManageBlocks],
priority: 11, priority: 11,
description: "newRole", description: "newRole",
visible: true, visible: true,
icon: "https://example.com/icon.png", icon: "https://example.com/icon.png",
}),
}); });
expect(response.status).toBe(403); expect(ok).toBe(false);
const output = await response.json(); expect(raw.status).toBe(403);
expect(output).toMatchObject({ expect(data).toMatchObject({
error: "Cannot create role with higher priority than your own", error: "Cannot create role with higher priority than your own",
}); });
}); });
test("should return 403 if user tries to create a role with permissions they do not have", async () => { test("should return 403 if user tries to create a role with permissions they do not have", async () => {
const response = await fakeRequest("/api/v1/roles", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { data, ok, raw } = await client.createRole("newRole", {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "newRole",
permissions: [RolePermissions.Impersonate], permissions: [RolePermissions.Impersonate],
priority: 1, priority: 1,
description: "newRole", description: "newRole",
visible: true, visible: true,
icon: "https://example.com/icon.png", icon: "https://example.com/icon.png",
}),
}); });
expect(response.status).toBe(403); expect(ok).toBe(false);
const output = await response.json(); expect(raw.status).toBe(403);
expect(output).toMatchObject({ expect(data).toMatchObject({
error: "Cannot create role with permissions you do not have", error: "Cannot create role with permissions you do not have",
details: "Forbidden permissions: impersonate", details: "Forbidden permissions: impersonate",
}); });

View file

@ -1,60 +1,43 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import type { Status as ApiStatus } from "@versia/client/types"; import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(5); const { users, deleteUsers } = await getTestUsers(5);
const timeline = (await getTestStatuses(2, users[0])).toReversed(); const timeline = (await getTestStatuses(2, users[0])).toReversed();
afterAll(async () => { afterAll(async () => {
await deleteUsers(); await deleteUsers();
}); });
// /api/v1/statuses/:id/favourite
describe("/api/v1/statuses/:id/favourite", () => { describe("/api/v1/statuses/:id/favourite", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest( await using client = await generateClient();
`/api/v1/statuses/${timeline[0].id}/favourite`,
{
method: "POST",
},
);
expect(response.status).toBe(401); const { ok, raw } = await client.favouriteStatus(timeline[0].id);
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should favourite post", async () => { test("should favourite post", async () => {
const response = await fakeRequest( await using client = await generateClient(users[1]);
`/api/v1/statuses/${timeline[0].id}/favourite`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const json = (await response.json()) as ApiStatus; const { data, ok } = await client.favouriteStatus(timeline[0].id);
expect(json.favourited).toBe(true); expect(ok).toBe(true);
expect(json.favourites_count).toBe(1); expect(data).toMatchObject({
favourited: true,
favourites_count: 1,
});
}); });
test("post should be favourited when fetched", async () => { test("post should be favourited when fetched", async () => {
const response = await fakeRequest( await using client = await generateClient(users[1]);
`/api/v1/statuses/${timeline[0].id}`,
{
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
},
);
expect(response.status).toBe(200); const { data } = await client.getStatus(timeline[0].id);
const json = (await response.json()) as ApiStatus; expect(data).toMatchObject({
favourited: true,
expect(json.favourited).toBe(true); favourites_count: 1,
expect(json.favourites_count).toBe(1); });
}); });
}); });

View file

@ -1,56 +1,42 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Account as ApiAccount } from "@versia/client/types"; import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(5); const { users, deleteUsers } = await getTestUsers(5);
const timeline = (await getTestStatuses(40, users[0])).toReversed(); const timeline = (await getTestStatuses(5, users[0])).toReversed();
afterAll(async () => { afterAll(async () => {
await deleteUsers(); await deleteUsers();
}); });
beforeAll(async () => { beforeAll(async () => {
await using client = await generateClient(users[1]);
for (const status of timeline) { for (const status of timeline) {
await fakeRequest(`/api/v1/statuses/${status.id}/favourite`, { await client.favouriteStatus(status.id);
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
});
} }
}); });
// /api/v1/statuses/:id/favourited_by // /api/v1/statuses/:id/favourited_by
describe("/api/v1/statuses/:id/favourited_by", () => { describe("/api/v1/statuses/:id/favourited_by", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest( await using client = await generateClient();
`/api/v1/statuses/${timeline[0].id}/favourited_by`,
);
expect(response.status).toBe(401); const { ok, raw } = await client.getStatusFavouritedBy(timeline[0].id);
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should return 200 with users", async () => { test("should return 200 with users", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/statuses/${timeline[0].id}/favourited_by`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200); const { data, ok } = await client.getStatusFavouritedBy(timeline[0].id);
expect(response.headers.get("content-type")).toContain(
"application/json",
);
const objects = (await response.json()) as ApiAccount[]; expect(ok).toBe(true);
expect(data).toBeArrayOfSize(1);
expect(objects.length).toBe(1); expect(data.length).toBe(1);
for (const [, status] of objects.entries()) { expect(data[0].id).toBe(users[1].id);
expect(status.id).toBe(users[1].id); expect(data[0].username).toBe(users[1].data.username);
expect(status.username).toBe(users[1].data.username);
}
}); });
}); });

View file

@ -1,56 +1,41 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Account as ApiAccount } from "@versia/client/types"; import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(5); const { users, deleteUsers } = await getTestUsers(5);
const timeline = (await getTestStatuses(40, users[0])).toReversed(); const timeline = (await getTestStatuses(5, users[0])).toReversed();
afterAll(async () => { afterAll(async () => {
await deleteUsers(); await deleteUsers();
}); });
beforeAll(async () => { beforeAll(async () => {
await using client = await generateClient(users[1]);
for (const status of timeline) { for (const status of timeline) {
await fakeRequest(`/api/v1/statuses/${status.id}/reblog`, { await client.reblogStatus(status.id);
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
});
} }
}); });
// /api/v1/statuses/:id/reblogged_by
describe("/api/v1/statuses/:id/reblogged_by", () => { describe("/api/v1/statuses/:id/reblogged_by", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest( await using client = await generateClient();
`/api/v1/statuses/${timeline[0].id}/reblogged_by`,
);
expect(response.status).toBe(401); const { ok, raw } = await client.getStatusRebloggedBy(timeline[0].id);
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should return 200 with users", async () => { test("should return 200 with users", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/statuses/${timeline[0].id}/reblogged_by`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200); const { data, ok } = await client.getStatusRebloggedBy(timeline[0].id);
expect(response.headers.get("content-type")).toContain(
"application/json",
);
const objects = (await response.json()) as ApiAccount[]; expect(ok).toBe(true);
expect(data).toBeArrayOfSize(1);
expect(objects.length).toBe(1); expect(data.length).toBe(1);
for (const [, status] of objects.entries()) { expect(data[0].id).toBe(users[1].id);
expect(status.id).toBe(users[1].id); expect(data[0].username).toBe(users[1].data.username);
expect(status.username).toBe(users[1].data.username);
}
}); });
}); });

View file

@ -1,84 +1,58 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import type { Status as ApiStatus } from "@versia/client/types"; import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(5); const { users, deleteUsers } = await getTestUsers(5);
const timeline = (await getTestStatuses(2, users[0])).toReversed(); const timeline = (await getTestStatuses(2, users[0])).toReversed();
afterAll(async () => { afterAll(async () => {
await deleteUsers(); await deleteUsers();
}); });
// /api/v1/statuses/:id/unfavourite
describe("/api/v1/statuses/:id/unfavourite", () => { describe("/api/v1/statuses/:id/unfavourite", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest( await using client = await generateClient();
`/api/v1/statuses/${timeline[0].id}/unfavourite`,
{
method: "POST",
},
);
expect(response.status).toBe(401); const { ok, raw } = await client.unfavouriteStatus(timeline[0].id);
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should be able to unfavourite post that is not favourited", async () => { test("should be able to unfavourite post that is not favourited", async () => {
const response = await fakeRequest( await using client = await generateClient(users[1]);
`/api/v1/statuses/${timeline[0].id}/unfavourite`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
},
);
expect(response.status).toBe(200); const { ok } = await client.unfavouriteStatus(timeline[0].id);
expect(ok).toBe(true);
}); });
test("should unfavourite post", async () => { test("should unfavourite post", async () => {
beforeAll(async () => { await using client = await generateClient(users[1]);
await fakeRequest(`/api/v1/statuses/${timeline[1].id}/favourite`, {
method: "POST", await client.favouriteStatus(timeline[1].id);
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`, const { ok } = await client.unfavouriteStatus(timeline[1].id);
},
expect(ok).toBe(true);
const { ok: ok2, data } = await client.getStatus(timeline[1].id);
expect(ok2).toBe(true);
expect(data).toMatchObject({
favourited: false,
favourites_count: 0,
}); });
}); });
const response = await fakeRequest(
`/api/v1/statuses/${timeline[1].id}/unfavourite`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
const json = (await response.json()) as ApiStatus;
expect(json.favourited).toBe(false);
expect(json.favourites_count).toBe(0);
});
test("post should not be favourited when fetched", async () => { test("post should not be favourited when fetched", async () => {
const response = await fakeRequest( await using client = await generateClient(users[1]);
`/api/v1/statuses/${timeline[1].id}`,
{
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
},
);
expect(response.status).toBe(200); const { ok, data } = await client.getStatus(timeline[1].id);
const json = (await response.json()) as ApiStatus; expect(ok).toBe(true);
expect(data).toMatchObject({
expect(json.favourited).toBe(false); favourited: false,
expect(json.favourites_count).toBe(0); favourites_count: 0,
});
}); });
}); });

View file

@ -1,12 +1,13 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Status as ApiStatus } from "@versia/client/types"; import type { z } from "@hono/zod-openapi";
import type { Status } from "@versia/client-ng/schemas";
import { Media, db } from "@versia/kit/db"; import { Media, db } from "@versia/kit/db";
import { Emojis } from "@versia/kit/tables"; import { Emojis } from "@versia/kit/tables";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { config } from "~/config.ts"; import { config } from "~/config.ts";
import { fakeRequest, getTestUsers } from "~/tests/utils"; import { generateClient, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(5); const { users, deleteUsers } = await getTestUsers(5);
let media: Media; let media: Media;
afterAll(async () => { afterAll(async () => {
@ -32,266 +33,172 @@ beforeAll(async () => {
describe("/api/v1/statuses", () => { describe("/api/v1/statuses", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient();
method: "POST",
body: new URLSearchParams({
status: "Hello, world!",
}),
});
expect(response.status).toBe(401); const { ok, raw } = await client.postStatus("Hello, world!");
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should return 422 is status is empty", async () => { test("should return 422 is status is empty", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams(),
});
expect(response.status).toBe(422); const { ok, raw } = await client.postStatus("");
expect(ok).toBe(false);
expect(raw.status).toBe(422);
}); });
test("should return 422 is status is too long", async () => { test("should return 422 is status is too long", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "a".repeat(config.validation.notes.max_characters + 1),
local_only: "true",
}),
});
expect(response.status).toBe(422); const { ok, raw } = await client.postStatus(
"a".repeat(config.validation.notes.max_characters + 1),
);
expect(ok).toBe(false);
expect(raw.status).toBe(422);
}); });
test("should return 422 is visibility is invalid", async () => { test("should return 422 is visibility is invalid", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { ok, raw } = await client.postStatus("Hello, world!", {
Authorization: `Bearer ${tokens[0].data.accessToken}`, visibility: "invalid" as z.infer<typeof Status>["visibility"],
},
body: new URLSearchParams({
status: "Hello, world!",
visibility: "invalid",
local_only: "true",
}),
}); });
expect(response.status).toBe(422); expect(ok).toBe(false);
expect(raw.status).toBe(422);
}); });
test("should return 422 if scheduled_at is invalid", async () => { test("should return 422 if scheduled_at is invalid", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { data, ok, raw } = await client.postStatus("Hello, world!", {
Authorization: `Bearer ${tokens[0].data.accessToken}`, scheduled_at: new Date(Date.now() - 1000),
},
body: new URLSearchParams({
status: "Hello, world!",
scheduled_at: "invalid",
local_only: "true",
}),
}); });
expect(response.status).toBe(422); expect(ok).toBe(false);
expect(raw.status).toBe(422);
expect(data).toMatchObject({
error: expect.stringContaining(
"must be at least 5 minutes in the future",
),
});
}); });
test("should return 422 is in_reply_to_id is invalid", async () => { test("should return 422 is in_reply_to_id is invalid", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { ok, raw } = await client.postStatus("Hello, world!", {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "Hello, world!",
in_reply_to_id: "invalid", in_reply_to_id: "invalid",
local_only: "true",
}),
}); });
expect(response.status).toBe(422); expect(ok).toBe(false);
expect(raw.status).toBe(422);
}); });
test("should return 422 is quote_id is invalid", async () => { test("should return 422 is quote_id is invalid", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { ok, raw } = await client.postStatus("Hello, world!", {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "Hello, world!",
quote_id: "invalid", quote_id: "invalid",
local_only: "true",
}),
}); });
expect(response.status).toBe(422); expect(ok).toBe(false);
expect(raw.status).toBe(422);
}); });
test("should return 422 is media_ids is invalid", async () => { test("should return 422 is media_ids is invalid", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { ok, raw } = await client.postStatus("Hello, world!", {
Authorization: `Bearer ${tokens[0].data.accessToken}`, media_ids: ["invalid"],
},
body: new URLSearchParams({
status: "Hello, world!",
"media_ids[]": "invalid",
local_only: "true",
}),
}); });
expect(response.status).toBe(422); expect(ok).toBe(false);
expect(raw.status).toBe(422);
}); });
test("should create a post", async () => { test("should create a post", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { data, ok } = await client.postStatus("Hello, world!");
Authorization: `Bearer ${tokens[0].data.accessToken}`,
}, expect(ok).toBe(true);
body: new URLSearchParams({ expect(data).toMatchObject({
status: "Hello, world!", content: "<p>Hello, world!</p>",
local_only: "true",
}),
}); });
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
);
const object = (await response.json()) as ApiStatus;
expect(object.content).toBe("<p>Hello, world!</p>");
}); });
test("should create a post with visibility", async () => { test("should create a post with visibility", async () => {
// This one uses JSON to test the interop await using client = await generateClient(users[0]);
const response = await fakeRequest("/api/v1/statuses", {
method: "POST", const { data, ok } = await client.postStatus("Hello, world!", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
status: "Hello, world!",
visibility: "unlisted", visibility: "unlisted",
local_only: "true",
}),
}); });
expect(response.status).toBe(200); expect(ok).toBe(true);
expect(response.headers.get("content-type")).toContain( expect(data).toMatchObject({
"application/json", visibility: "unlisted",
); content: "<p>Hello, world!</p>",
});
const object = (await response.json()) as ApiStatus;
expect(object.content).toBe("<p>Hello, world!</p>");
expect(object.visibility).toBe("unlisted");
}); });
test("should create a post with a reply", async () => { test("should create a post with a reply", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { data, ok } = await client.postStatus("Hello, world!");
Authorization: `Bearer ${tokens[0].data.accessToken}`,
expect(ok).toBe(true);
const { data: data2, ok: ok2 } = await client.postStatus(
"Hello, world again!",
{
in_reply_to_id: data.id,
}, },
body: new URLSearchParams({
status: "Hello, world!",
local_only: "true",
}),
});
const object = (await response.json()) as ApiStatus;
const response2 = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "Hello, world again!",
in_reply_to_id: object.id,
local_only: "true",
}),
});
expect(response2.status).toBe(200);
expect(response2.headers.get("content-type")).toContain(
"application/json",
); );
const object2 = (await response2.json()) as ApiStatus; expect(ok2).toBe(true);
expect(data2).toMatchObject({
expect(object2.content).toBe("<p>Hello, world again!</p>"); content: "<p>Hello, world again!</p>",
expect(object2.in_reply_to_id).toBe(object.id); in_reply_to_id: data.id,
});
}); });
test("should create a post with a quote", async () => { test("should create a post with a quote", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { data, ok } = await client.postStatus("Hello, world!");
Authorization: `Bearer ${tokens[0].data.accessToken}`,
expect(ok).toBe(true);
const { data: data2, ok: ok2 } = await client.postStatus(
"Hello, world again!",
{
quote_id: data.id,
}, },
body: new URLSearchParams({
status: "Hello, world!",
local_only: "true",
}),
});
const object = (await response.json()) as ApiStatus;
const response2 = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "Hello, world again!",
quote_id: object.id,
local_only: "true",
}),
});
expect(response2.status).toBe(200);
expect(response2.headers.get("content-type")).toContain(
"application/json",
); );
const object2 = (await response2.json()) as ApiStatus; expect(ok2).toBe(true);
expect(data2).toMatchObject({
expect(object2.content).toBe("<p>Hello, world again!</p>"); content: "<p>Hello, world again!</p>",
expect(object2.quote?.id).toBe(object.id); quote: expect.objectContaining({
id: data.id,
}),
});
}); });
test("should correctly parse emojis", async () => { test("should correctly parse emojis", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "Hello, :test:!",
local_only: "true",
}),
});
expect(response.status).toBe(200); const { data, ok } = await client.postStatus("Hello, :test:!");
expect(response.headers.get("content-type")).toContain(
"application/json",
);
const object = (await response.json()) as ApiStatus; expect(ok).toBe(true);
expect(object.emojis).toBeArrayOfSize(1); expect((data as z.infer<typeof Status>).emojis).toBeArrayOfSize(1);
expect(object.emojis[0]).toMatchObject({ expect((data as z.infer<typeof Status>).emojis[0]).toMatchObject({
shortcode: "test", shortcode: "test",
url: expect.stringContaining("/media/proxy/"), url: expect.stringContaining("/media/proxy/"),
}); });
@ -299,29 +206,20 @@ describe("/api/v1/statuses", () => {
describe("mentions testing", () => { describe("mentions testing", () => {
test("should correctly parse @mentions", async () => { test("should correctly parse @mentions", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { data, ok } = await client.postStatus(
Authorization: `Bearer ${tokens[0].data.accessToken}`, `Hello, @${users[1].data.username}!`,
}, );
body: new URLSearchParams({
status: `Hello, @${users[1].data.username}!`, expect(ok).toBe(true);
local_only: "true", expect(data).toMatchObject({
}), content: `<p>Hello, <a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${users[1].getUri()}">@${users[1].data.username}</a>!</p>`,
}); });
expect((data as z.infer<typeof Status>).mentions).toBeArrayOfSize(
expect(response.status).toBe(200); 1,
expect(response.headers.get("content-type")).toContain(
"application/json",
); );
expect((data as z.infer<typeof Status>).mentions[0]).toMatchObject({
const object = (await response.json()) as ApiStatus;
expect(object.content).toBe(
`<p>Hello, <a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${users[1].getUri()}">@${users[1].data.username}</a>!</p>`,
);
expect(object.mentions).toBeArrayOfSize(1);
expect(object.mentions[0]).toMatchObject({
id: users[1].id, id: users[1].id,
username: users[1].data.username, username: users[1].data.username,
acct: users[1].data.username, acct: users[1].data.username,
@ -329,28 +227,22 @@ describe("/api/v1/statuses", () => {
}); });
test("should correctly parse @mentions@domain", async () => { test("should correctly parse @mentions@domain", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { data, ok } = await client.postStatus(
Authorization: `Bearer ${tokens[0].data.accessToken}`, `Hello, @${users[1].data.username}@${
},
body: new URLSearchParams({
status: `Hello, @${users[1].data.username}@${
config.http.base_url.host config.http.base_url.host
}!`, }!`,
local_only: "true",
}),
});
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
); );
const object = (await response.json()) as ApiStatus; expect(ok).toBe(true);
expect(data).toMatchObject({
expect(object.mentions).toBeArrayOfSize(1); content: `<p>Hello, <a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${users[1].getUri()}">@${users[1].data.username}</a>!</p>`,
expect(object.mentions[0]).toMatchObject({ });
expect((data as z.infer<typeof Status>).mentions).toBeArrayOfSize(
1,
);
expect((data as z.infer<typeof Status>).mentions[0]).toMatchObject({
id: users[1].id, id: users[1].id,
username: users[1].data.username, username: users[1].data.username,
acct: users[1].data.username, acct: users[1].data.username,
@ -360,83 +252,51 @@ describe("/api/v1/statuses", () => {
describe("HTML injection testing", () => { describe("HTML injection testing", () => {
test("should not allow HTML injection", async () => { test("should not allow HTML injection", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "Hi! <script>alert('Hello, world!');</script>",
local_only: "true",
}),
});
expect(response.status).toBe(200); const { data, ok } = await client.postStatus(
expect(response.headers.get("content-type")).toContain( "Hi! <script>alert('Hello, world!');</script>",
"application/json",
); );
const object = (await response.json()) as ApiStatus; expect(ok).toBe(true);
expect(data).toMatchObject({
expect(object.content).toBe( content:
"<p>Hi! &lt;script&gt;alert('Hello, world!');&lt;/script&gt;</p>", "<p>Hi! &lt;script&gt;alert('Hello, world!');&lt;/script&gt;</p>",
); });
}); });
test("should not allow HTML injection in spoiler_text", async () => { test("should not allow HTML injection in spoiler_text", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { data, ok } = await client.postStatus("Hello, world!", {
Authorization: `Bearer ${tokens[0].data.accessToken}`, spoiler_text: "uwu <script>alert('Hello, world!');</script>",
},
body: new URLSearchParams({
status: "Hello, world!",
spoiler_text:
"uwu <script>alert('Hello, world!');</script>",
local_only: "true",
}),
}); });
expect(response.status).toBe(200); expect(ok).toBe(true);
expect(response.headers.get("content-type")).toContain( expect(data).toMatchObject({
"application/json", spoiler_text:
);
const object = (await response.json()) as ApiStatus;
expect(object.spoiler_text).toBe(
"uwu &#x3C;script&#x3E;alert(&#x27;Hello, world!&#x27;);&#x3C;/script&#x3E;", "uwu &#x3C;script&#x3E;alert(&#x27;Hello, world!&#x27;);&#x3C;/script&#x3E;",
); });
}); });
test("should rewrite all image and video src to go through proxy", async () => { test("should rewrite all image and video src to go through proxy", async () => {
const response = await fakeRequest("/api/v1/statuses", { await using client = await generateClient(users[0]);
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "<img src='https://example.com/image.jpg'> <video src='https://example.com/video.mp4'> Test!",
local_only: "true",
}),
});
expect(response.status).toBe(200); const { data, ok } = await client.postStatus(
expect(response.headers.get("content-type")).toContain( "<img src='https://example.com/image.jpg'> <video src='https://example.com/video.mp4'> Test!",
"application/json",
); );
const object = (await response.json()) as ApiStatus; expect(ok).toBe(true);
// Proxy url is base_url/media/proxy/<base64url encoded url> // Proxy url is base_url/media/proxy/<base64url encoded url>
expect(object.content).toBe( expect(data).toMatchObject({
`<p><img src="${config.http.base_url}media/proxy/${Buffer.from( content: `<p><img src="${config.http.base_url}media/proxy/${Buffer.from(
"https://example.com/image.jpg", "https://example.com/image.jpg",
).toString("base64url")}"> <video src="${ ).toString("base64url")}"> <video src="${
config.http.base_url config.http.base_url
}media/proxy/${Buffer.from( }media/proxy/${Buffer.from(
"https://example.com/video.mp4", "https://example.com/video.mp4",
).toString("base64url")}"> Test!</p>`, ).toString("base64url")}"> Test!</p>`,
); });
}); });
}); });
}); });

View file

@ -1,10 +1,9 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import type { Status as ApiStatus } from "@versia/client/types";
import { config } from "~/config.ts"; import { config } from "~/config.ts";
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils"; import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(5); const { users, deleteUsers } = await getTestUsers(5);
const timeline = (await getTestStatuses(40, users[0])).toReversed(); const timeline = (await getTestStatuses(10, users[0])).toReversed();
afterAll(async () => { afterAll(async () => {
await deleteUsers(); await deleteUsers();
@ -12,44 +11,35 @@ afterAll(async () => {
describe("/api/v1/timelines/home", () => { describe("/api/v1/timelines/home", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest("/api/v1/timelines/home"); await using client = await generateClient();
expect(response.status).toBe(401); const { ok, raw } = await client.getHomeTimeline();
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should correctly parse limit", async () => { test("should correctly parse limit", async () => {
const response = await fakeRequest("/api/v1/timelines/home?limit=5", { await using client = await generateClient(users[0]);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`, const { data, ok } = await client.getHomeTimeline({
}, limit: 2,
}); });
expect(response.status).toBe(200); expect(ok).toBe(true);
expect(response.headers.get("content-type")).toContain( expect(data).toBeArrayOfSize(2);
"application/json",
);
const objects = (await response.json()) as ApiStatus[];
expect(objects.length).toBe(5);
}); });
test("should return 200 with statuses", async () => { test("should return 200 with statuses", async () => {
const response = await fakeRequest("/api/v1/timelines/home", { await using client = await generateClient(users[0]);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`, const { data, ok } = await client.getHomeTimeline({
}, limit: 5,
}); });
expect(response.status).toBe(200); expect(ok).toBe(true);
expect(response.headers.get("content-type")).toContain( expect(data).toBeArrayOfSize(5);
"application/json", for (const [index, status] of data.entries()) {
);
const objects = (await response.json()) as ApiStatus[];
expect(objects.length).toBe(20);
for (const [index, status] of objects.entries()) {
expect(status.account).toBeDefined(); expect(status.account).toBeDefined();
expect(status.account.id).toBe(users[0].id); expect(status.account.id).toBe(users[0].id);
expect(status.content).toBeDefined(); expect(status.content).toBeDefined();
@ -60,81 +50,64 @@ describe("/api/v1/timelines/home", () => {
describe("should paginate properly", () => { describe("should paginate properly", () => {
test("should send correct Link header", async () => { test("should send correct Link header", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
"/api/v1/timelines/home?limit=20",
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.headers.get("link")).toBe( const { data, ok, raw } = await client.getHomeTimeline({
`<${config.http.base_url}api/v1/timelines/home?limit=20&max_id=${timeline[19].id}>; rel="next"`, limit: 5,
});
expect(ok).toBe(true);
expect(data).toBeArrayOfSize(5);
expect(raw.headers.get("link")).toBe(
`<${config.http.base_url}api/v1/timelines/home?limit=5&max_id=${timeline[4].id}>; rel="next"`,
); );
}); });
test("should correct statuses with max", async () => { test("should correct statuses with max", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/timelines/home?limit=20&max_id=${timeline[19].id}`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200); const { data, ok } = await client.getHomeTimeline({
expect(response.headers.get("content-type")).toContain( limit: 5,
"application/json", max_id: timeline[4].id,
); });
const objects = (await response.json()) as ApiStatus[]; expect(ok).toBe(true);
expect(data).toBeArrayOfSize(5);
expect(objects.length).toBe(20); for (const [index, status] of data.entries()) {
for (const [index, status] of objects.entries()) {
expect(status.account).toBeDefined(); expect(status.account).toBeDefined();
expect(status.account.id).toBe(users[0].id); expect(status.account.id).toBe(users[0].id);
expect(status.content).toBeDefined(); expect(status.content).toBeDefined();
expect(status.created_at).toBeDefined(); expect(status.created_at).toBeDefined();
expect(status.id).toBe(timeline[index + 20].id); expect(status.id).toBe(timeline[index + 5].id);
} }
}); });
test("should send correct Link prev header", async () => { test("should send correct Link prev header", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/timelines/home?limit=20&max_id=${timeline[19].id}`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.headers.get("link")).toInclude( const { data, ok, raw } = await client.getHomeTimeline({
`${config.http.base_url}api/v1/timelines/home?limit=20&min_id=${timeline[20].id}>; rel="prev"`, limit: 5,
max_id: timeline[4].id,
});
expect(ok).toBe(true);
expect(data).toBeArrayOfSize(5);
expect(raw.headers.get("link")).toInclude(
`<${config.http.base_url}api/v1/timelines/home?limit=5&min_id=${timeline[5].id}>; rel="prev"`,
); );
}); });
test("should correct statuses with min_id", async () => { test("should correct statuses with min_id", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/timelines/home?limit=20&min_id=${timeline[20].id}`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200); const { data, ok } = await client.getHomeTimeline({
expect(response.headers.get("content-type")).toContain( limit: 5,
"application/json", min_id: timeline[5].id,
); });
const objects = (await response.json()) as ApiStatus[]; expect(ok).toBe(true);
expect(data).toBeArrayOfSize(5);
expect(objects.length).toBe(20); for (const [index, status] of data.entries()) {
for (const [index, status] of objects.entries()) {
expect(status.account).toBeDefined(); expect(status.account).toBeDefined();
expect(status.account.id).toBe(users[0].id); expect(status.account.id).toBe(users[0].id);
expect(status.content).toBeDefined(); expect(status.content).toBeDefined();
@ -144,58 +117,39 @@ describe("/api/v1/timelines/home", () => {
}); });
test("should not return statuses with filtered keywords", async () => { test("should not return statuses with filtered keywords", async () => {
const filterResponse = await fakeRequest("/api/v2/filters", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { data, ok } = await client.createFilter(
Authorization: `Bearer ${tokens[0].data.accessToken}`, ["home"],
"Content-Type": "application/x-www-form-urlencoded", "Test Filter",
"hide",
{
keywords_attributes: [
{
keyword: timeline[0].content.slice(4, 20),
whole_word: false,
}, },
body: new URLSearchParams({ ],
title: "Test Filter", },
"context[]": "home", );
filter_action: "hide",
"keywords_attributes[0][keyword]": expect(ok).toBe(true);
timeline[0].content.slice(4, 20),
"keywords_attributes[0][whole_word]": "false", const { data: data2, ok: ok2 } = await client.getHomeTimeline({
}), limit: 5,
}); });
expect(filterResponse.status).toBe(200); expect(ok2).toBe(true);
expect(data2).toBeArrayOfSize(5);
const response = await fakeRequest(
"/api/v1/timelines/home?limit=20",
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
);
const objects = (await response.json()) as ApiStatus[];
expect(objects.length).toBe(20);
// There should be no element with id of timeline[0].id // There should be no element with id of timeline[0].id
expect(objects).not.toContainEqual( expect(data2).not.toContainEqual(
expect.objectContaining({ id: timeline[0].id }), expect.objectContaining({ id: timeline[0].id }),
); );
// Delete filter // Delete filter
const filterDeleteResponse = await fakeRequest( const { ok: ok3 } = await client.deleteFilter(data.id);
`/api/v2/filters/${(await filterResponse.json()).id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(filterDeleteResponse.status).toBe(204); expect(ok3).toBe(true);
}); });
}); });
}); });

View file

@ -1,10 +1,9 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import type { Status as ApiStatus } from "@versia/client/types";
import { config } from "~/config.ts"; import { config } from "~/config.ts";
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils"; import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(5); const { users, deleteUsers } = await getTestUsers(5);
const timeline = (await getTestStatuses(40, users[0])).toReversed(); const timeline = (await getTestStatuses(10, users[0])).toReversed();
afterAll(async () => { afterAll(async () => {
await deleteUsers(); await deleteUsers();
@ -12,38 +11,26 @@ afterAll(async () => {
describe("/api/v1/timelines/public", () => { describe("/api/v1/timelines/public", () => {
test("should correctly parse limit", async () => { test("should correctly parse limit", async () => {
const response = await fakeRequest("/api/v1/timelines/public?limit=5", { await using client = await generateClient(users[0]);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`, const { data, ok } = await client.getPublicTimeline({
}, limit: 5,
}); });
expect(response.status).toBe(200); expect(ok).toBe(true);
expect(response.headers.get("content-type")).toContain( expect(data).toBeArrayOfSize(5);
"application/json",
);
const objects = (await response.json()) as ApiStatus[];
expect(objects.length).toBe(5);
}); });
test("should return 200 with statuses", async () => { test("should return 200 with statuses", async () => {
const response = await fakeRequest("/api/v1/timelines/public", { await using client = await generateClient(users[0]);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`, const { data, ok } = await client.getPublicTimeline({
}, limit: 5,
}); });
expect(response.status).toBe(200); expect(ok).toBe(true);
expect(response.headers.get("content-type")).toContain( expect(data).toBeArrayOfSize(5);
"application/json", for (const [index, status] of data.entries()) {
);
const objects = (await response.json()) as ApiStatus[];
expect(objects.length).toBe(20);
for (const [index, status] of objects.entries()) {
expect(status.account).toBeDefined(); expect(status.account).toBeDefined();
expect(status.account.id).toBe(users[0].id); expect(status.account.id).toBe(users[0].id);
expect(status.content).toBeDefined(); expect(status.content).toBeDefined();
@ -53,24 +40,16 @@ describe("/api/v1/timelines/public", () => {
}); });
test("should only fetch local statuses", async () => { test("should only fetch local statuses", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
"/api/v1/timelines/public?limit=20&local=true",
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200); const { data, ok } = await client.getPublicTimeline({
expect(response.headers.get("content-type")).toContain( limit: 5,
"application/json", local: true,
); });
const objects = (await response.json()) as ApiStatus[]; expect(ok).toBe(true);
expect(data).toBeArrayOfSize(5);
expect(objects.length).toBe(20); for (const [index, status] of data.entries()) {
for (const [index, status] of objects.entries()) {
expect(status.account).toBeDefined(); expect(status.account).toBeDefined();
expect(status.account.id).toBe(users[0].id); expect(status.account.id).toBe(users[0].id);
expect(status.content).toBeDefined(); expect(status.content).toBeDefined();
@ -80,102 +59,77 @@ describe("/api/v1/timelines/public", () => {
}); });
test("should only fetch remote statuses", async () => { test("should only fetch remote statuses", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
"/api/v1/timelines/public?remote=true&allow_local_only=false&only_media=false",
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200); const { data, ok } = await client.getPublicTimeline({
expect(response.headers.get("content-type")).toContain( limit: 5,
"application/json", remote: true,
); });
const objects = (await response.json()) as ApiStatus[]; expect(ok).toBe(true);
expect(data).toBeArrayOfSize(0);
expect(objects).toBeArray();
}); });
describe("should paginate properly", () => { describe("should paginate properly", () => {
test("should send correct Link header", async () => { test("should send correct Link header", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
"/api/v1/timelines/public?limit=20",
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.headers.get("link")).toBe( const { data, ok, raw } = await client.getPublicTimeline({
`<${config.http.base_url}api/v1/timelines/public?limit=20&max_id=${timeline[19].id}>; rel="next"`, limit: 5,
});
expect(ok).toBe(true);
expect(data).toBeArrayOfSize(5);
expect(raw.headers.get("link")).toBe(
`<${config.http.base_url}api/v1/timelines/public?limit=5&max_id=${timeline[4].id}>; rel="next"`,
); );
}); });
test("should correct statuses with max", async () => { test("should correct statuses with max", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/timelines/public?limit=20&max_id=${timeline[19].id}`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200); const { data, ok } = await client.getPublicTimeline({
expect(response.headers.get("content-type")).toContain( limit: 5,
"application/json", max_id: timeline[4].id,
); });
const objects = (await response.json()) as ApiStatus[]; expect(ok).toBe(true);
expect(data).toBeArrayOfSize(5);
expect(objects.length).toBe(20); for (const [index, status] of data.entries()) {
for (const [index, status] of objects.entries()) {
expect(status.account).toBeDefined(); expect(status.account).toBeDefined();
expect(status.account.id).toBe(users[0].id); expect(status.account.id).toBe(users[0].id);
expect(status.content).toBeDefined(); expect(status.content).toBeDefined();
expect(status.created_at).toBeDefined(); expect(status.created_at).toBeDefined();
expect(status.id).toBe(timeline[index + 20].id); expect(status.id).toBe(timeline[index + 5].id);
} }
}); });
test("should send correct Link prev header", async () => { test("should send correct Link prev header", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/timelines/public?limit=20&max_id=${timeline[19].id}`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.headers.get("link")).toInclude( const { data, ok, raw } = await client.getPublicTimeline({
`${config.http.base_url}api/v1/timelines/public?limit=20&min_id=${timeline[20].id}>; rel="prev"`, limit: 5,
max_id: timeline[4].id,
});
expect(ok).toBe(true);
expect(data).toBeArrayOfSize(5);
expect(raw.headers.get("link")).toInclude(
`<${config.http.base_url}api/v1/timelines/public?limit=5&min_id=${timeline[5].id}>; rel="prev"`,
); );
}); });
test("should correct statuses with min_id", async () => { test("should correct statuses with min_id", async () => {
const response = await fakeRequest( await using client = await generateClient(users[0]);
`/api/v1/timelines/public?limit=20&min_id=${timeline[20].id}`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200); const { data, ok } = await client.getPublicTimeline({
expect(response.headers.get("content-type")).toContain( limit: 5,
"application/json", min_id: timeline[5].id,
); });
const objects = (await response.json()) as ApiStatus[]; expect(ok).toBe(true);
expect(data).toBeArrayOfSize(5);
expect(objects.length).toBe(20); for (const [index, status] of data.entries()) {
for (const [index, status] of objects.entries()) {
expect(status.account).toBeDefined(); expect(status.account).toBeDefined();
expect(status.account.id).toBe(users[0].id); expect(status.account.id).toBe(users[0].id);
expect(status.content).toBeDefined(); expect(status.content).toBeDefined();
@ -186,59 +140,38 @@ describe("/api/v1/timelines/public", () => {
}); });
test("should not return statuses with filtered keywords", async () => { test("should not return statuses with filtered keywords", async () => {
const filterResponse = await fakeRequest("/api/v2/filters", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { data, ok } = await client.createFilter(
Authorization: `Bearer ${tokens[0].data.accessToken}`, ["public"],
"Content-Type": "application/x-www-form-urlencoded", "Test Filter",
"hide",
{
keywords_attributes: [
{
keyword: timeline[0].content.slice(4, 20),
whole_word: false,
}, },
body: new URLSearchParams({ ],
title: "Test Filter", },
"context[]": "public", );
filter_action: "hide",
"keywords_attributes[0][keyword]": timeline[0].content.slice( expect(ok).toBe(true);
4,
20, const { data: data2, ok: ok2 } = await client.getPublicTimeline({
), limit: 5,
"keywords_attributes[0][whole_word]": "false",
}),
}); });
expect(filterResponse.status).toBe(200); expect(ok2).toBe(true);
expect(data2).toBeArrayOfSize(5);
const response = await fakeRequest(
"/api/v1/timelines/public?limit=20",
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
);
const objects = (await response.json()) as ApiStatus[];
expect(objects.length).toBe(20);
// There should be no element with id of timeline[0].id // There should be no element with id of timeline[0].id
expect(objects).not.toContainEqual( expect(data2).not.toContainEqual(
expect.objectContaining({ id: timeline[0].id }), expect.objectContaining({ id: timeline[0].id }),
); );
// Delete filter // Delete filter
const filterDeleteResponse = await fakeRequest( const { ok: ok3 } = await client.deleteFilter(data.id);
`/api/v2/filters/${(await filterResponse.json()).id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
expect(filterDeleteResponse.status).toBe(204); expect(ok3).toBe(true);
}); });
}); });

View file

@ -1,149 +1,121 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { fakeRequest, getTestUsers } from "~/tests/utils"; import { generateClient, getTestUsers } from "~/tests/utils";
const { tokens, deleteUsers } = await getTestUsers(2); const { users, deleteUsers } = await getTestUsers(2);
const response = await fakeRequest("/api/v2/filters", { await using client = await generateClient(users[0]);
method: "POST",
headers: { const { data: filter, ok } = await client.createFilter(
Authorization: `Bearer ${tokens[0].data.accessToken}`, ["home"],
"Content-Type": "application/x-www-form-urlencoded", "Test Filter",
"warn",
{
expires_in: 86400,
keywords_attributes: [{ keyword: "test", whole_word: true }],
}, },
body: new URLSearchParams({ );
title: "Test Filter",
"context[]": "home",
filter_action: "warn",
expires_in: "86400",
"keywords_attributes[0][keyword]": "test",
"keywords_attributes[0][whole_word]": "true",
}),
});
expect(response.status).toBe(200); expect(ok).toBe(true);
const filter = await response.json();
expect(filter).toBeObject(); expect(filter).toBeObject();
expect(filter).toContainKeys(["id", "title"]);
expect(filter.title).toBe("Test Filter");
afterAll(async () => { afterAll(async () => {
await deleteUsers(); await deleteUsers();
}); });
// /api/v2/filters/:id
describe("/api/v2/filters/:id", () => { describe("/api/v2/filters/:id", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(`/api/v2/filters/${filter.id}`, { await using client = await generateClient();
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
expect(response.status).toBe(401); const { ok, raw } = await client.getFilter(filter.id);
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should get that filter", async () => { test("should get that filter", async () => {
const response = await fakeRequest(`/api/v2/filters/${filter.id}`, { await using client = await generateClient(users[0]);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.status).toBe(200); const { data, ok } = await client.getFilter(filter.id);
const json = await response.json(); expect(ok).toBe(true);
expect(json).toBeObject(); expect(data).toBeObject();
expect(json).toContainKeys(["id", "title"]); expect(data).toContainKeys(["id", "title"]);
expect(json.title).toBe("Test Filter"); expect(data.title).toBe("Test Filter");
expect(json.context).toEqual(["home"]); expect(data.context).toEqual(["home"]);
expect(json.filter_action).toBe("warn"); expect(data.filter_action).toBe("warn");
expect(json.expires_at).toBeString(); expect(data.expires_at).toBeString();
expect(json.keywords).toBeArray(); expect(data.keywords).toBeArray();
expect(json.keywords).not.toBeEmpty(); expect(data.keywords).not.toBeEmpty();
expect(json.keywords[0]).toContainKeys(["keyword", "whole_word"]); expect(data.keywords[0]).toContainKeys(["keyword", "whole_word"]);
expect(json.keywords[0].keyword).toEqual("test"); expect(data.keywords[0].keyword).toEqual("test");
}); });
test("should edit that filter", async () => { test("should edit that filter", async () => {
const response = await fakeRequest(`/api/v2/filters/${filter.id}`, { await using client = await generateClient(users[0]);
method: "PUT",
headers: { const { data, ok } = await client.updateFilter(filter.id, {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
title: "New Filter", title: "New Filter",
"context[]": "notifications", context: ["notifications"],
filter_action: "hide", filter_action: "hide",
expires_in: "86400", expires_in: 86400,
"keywords_attributes[0][keyword]": "new", keywords_attributes: [
"keywords_attributes[0][id]": filter.keywords[0].id, {
"keywords_attributes[0][whole_word]": "false", id: filter.keywords[0].id,
}), keyword: "new",
whole_word: false,
},
],
}); });
expect(response.status).toBe(200); expect(ok).toBe(true);
expect(data).toBeObject();
const json = await response.json(); expect(data).toContainKeys(["id", "title"]);
expect(json).toBeObject(); expect(data.title).toBe("New Filter");
expect(json).toContainKeys(["id", "title"]); expect(data.context).toEqual(["notifications"]);
expect(json.title).toBe("New Filter"); expect(data.filter_action).toBe("hide");
expect(json.context).toEqual(["notifications"]); expect(data.expires_at).toBeString();
expect(json.filter_action).toBe("hide"); expect(data.keywords).toBeArray();
expect(json.expires_at).toBeString(); expect(data.keywords).not.toBeEmpty();
expect(json.keywords).toBeArray(); expect(data.keywords[0]).toContainKeys(["keyword", "whole_word"]);
expect(json.keywords).not.toBeEmpty(); expect(data.keywords[0].keyword).toEqual("new");
expect(json.keywords[0]).toContainKeys(["keyword", "whole_word"]);
expect(json.keywords[0].keyword).toEqual("new");
}); });
test("should delete keyword", async () => { test("should delete keyword", async () => {
const response = await fakeRequest(`/api/v2/filters/${filter.id}`, { await using client = await generateClient(users[0]);
method: "PUT",
headers: { const { data, ok } = await client.updateFilter(filter.id, {
Authorization: `Bearer ${tokens[0].data.accessToken}`, keywords_attributes: [
}, // biome-ignore lint/style/useNamingConvention: _destroy is a Mastodon API imposed variable name
body: new URLSearchParams({ { id: filter.keywords[0].id, _destroy: true },
"keywords_attributes[0][id]": filter.keywords[0].id, ],
"keywords_attributes[0][_destroy]": "true",
}),
}); });
expect(response.status).toBe(200); expect(ok).toBe(true);
expect(data).toBeObject();
expect(data).toContainKeys(["id", "title"]);
expect(data.title).toBe("New Filter");
const json = await response.json(); // Get the filter again and check that the keyword is deleted
expect(json).toBeObject(); const { data: data2, ok: ok2 } = await client.getFilter(filter.id);
expect(json.keywords).toBeEmpty();
// Get the filter again and check expect(ok2).toBe(true);
const getResponse = await fakeRequest(`/api/v2/filters/${filter.id}`, { expect(data2.keywords).toBeArray();
headers: { expect(data2.keywords).toBeEmpty();
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(getResponse.status).toBe(200);
expect((await getResponse.json()).keywords).toBeEmpty();
}); });
test("should delete filter", async () => { test("should delete filter", async () => {
const formData = new FormData(); await using client = await generateClient(users[0]);
const response = await fakeRequest(`/api/v2/filters/${filter.id}`, { const { ok } = await client.deleteFilter(filter.id);
method: "DELETE",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: formData,
});
expect(response.status).toBe(204); expect(ok).toBe(true);
// Try to GET the filter again // Try to GET the filter again
const getResponse = await fakeRequest(`/api/v2/filters/${filter.id}`, { const { ok: ok2, raw } = await client.getFilter(filter.id);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(getResponse.status).toBe(404); expect(ok2).toBe(false);
expect(raw.status).toBe(404);
}); });
}); });

View file

@ -1,72 +1,54 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { fakeRequest, getTestUsers } from "~/tests/utils"; import { generateClient, getTestUsers } from "~/tests/utils";
const { tokens, deleteUsers } = await getTestUsers(2); const { users, deleteUsers } = await getTestUsers(2);
afterAll(async () => { afterAll(async () => {
await deleteUsers(); await deleteUsers();
}); });
// /api/v2/filters
describe("/api/v2/filters", () => { describe("/api/v2/filters", () => {
test("should return 401 if not authenticated", async () => { test("should return 401 if not authenticated", async () => {
const response = await fakeRequest("/api/v2/filters"); await using client = await generateClient();
expect(response.status).toBe(401); const { ok, raw } = await client.getFilters();
expect(ok).toBe(false);
expect(raw.status).toBe(401);
}); });
test("should return user filters (none)", async () => { test("should return user filters (none)", async () => {
const response = await fakeRequest("/api/v2/filters", { await using client = await generateClient(users[0]);
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.status).toBe(200); const { data, ok } = await client.getFilters();
const json = await response.json(); expect(ok).toBe(true);
expect(json).toBeArray(); expect(data).toBeArrayOfSize(0);
expect(json).toBeEmpty();
}); });
test("should create a new filter", async () => { test("should create a new filter", async () => {
const formData = new FormData(); await using client = await generateClient(users[0]);
formData.append("title", "Test Filter"); const { data, ok } = await client.createFilter(
formData.append("context[]", "home"); ["home"],
formData.append("filter_action", "warn"); "Test Filter",
formData.append("expires_in", "86400"); "warn",
formData.append("keywords_attributes[0][keyword]", "test"); {
formData.append("keywords_attributes[0][whole_word]", "true"); expires_in: 86400,
keywords_attributes: [{ keyword: "test", whole_word: true }],
const response = await fakeRequest("/api/v2/filters", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/x-www-form-urlencoded",
}, },
body: new URLSearchParams({ );
title: "Test Filter",
"context[]": "home",
filter_action: "warn",
expires_in: "86400",
"keywords_attributes[0][keyword]": "test",
"keywords_attributes[0][whole_word]": "true",
}),
});
expect(response.status).toBe(200); expect(ok).toBe(true);
expect(data).toBeObject();
const json = await response.json(); expect(data).toContainKeys(["id", "title"]);
expect(json).toBeObject(); expect(data.title).toBe("Test Filter");
expect(json).toContainKeys(["id", "title"]); expect(data.context).toEqual(["home"]);
expect(json.title).toBe("Test Filter"); expect(data.filter_action).toBe("warn");
expect(json.context).toEqual(["home"]); expect(data.expires_at).toBeString();
expect(json.filter_action).toBe("warn"); expect(data.keywords).toBeArray();
expect(json.expires_at).toBeString(); expect(data.keywords).not.toBeEmpty();
expect(json.keywords).toBeArray(); expect(data.keywords[0]).toContainKeys(["keyword", "whole_word"]);
expect(json.keywords).not.toBeEmpty(); expect(data.keywords[0].keyword).toEqual("test");
expect(json.keywords[0]).toContainKeys(["keyword", "whole_word"]);
expect(json.keywords[0].keyword).toEqual("test");
}); });
}); });

View file

@ -1,16 +1,16 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { fakeRequest } from "~/tests/utils"; import { generateClient } from "~/tests/utils";
// /api/v2/instance // /api/v2/instance
describe("/api/v2/instance", () => { describe("/api/v2/instance", () => {
test("should return instance information", async () => { test("should return instance information", async () => {
const response = await fakeRequest("/api/v2/instance"); await using client = await generateClient();
expect(response.status).toBe(200); const { data, ok } = await client.getInstance();
const json = await response.json(); expect(ok).toBe(true);
expect(json).toBeObject(); expect(data).toBeObject();
expect(json).toContainKeys([ expect(data).toContainKeys([
"domain", "domain",
"title", "title",
"version", "version",

View file

@ -364,3 +364,176 @@ export const Status = z.object({
}, },
}), }),
}); });
/*
Attributes
id
Description: ID of the scheduled status in the database.
Type: String (cast from an integer but not guaranteed to be a number)
Version history:
2.7.0 - added
scheduled_at
Description: The timestamp for when the status will be posted.
Type: String (Datetime)
Version history:
2.7.0 - added
params
Description: The parameters that were used when scheduling the status, to be used when the status is posted.
Type: Hash
Version history:
2.7.0 - added
params[text]
Description: Text to be used as status content.
Type: String
Version history:
2.7.0 - added
params[poll]
Description: Poll to be attached to the status.
Type: nullable Hash
Version history:
2.8.0 - added
params[poll][options[]]
Description: The poll options to be used.
Type: Array of String
Version history:
2.8.0 - added
params[poll][expires_in]
Description: How many seconds the poll should last before closing.
Type: String (cast from integer)
Version history:
2.8.0 - added
params[poll][multiple]
Description: Whether the poll allows multiple choices.
Type: optional Boolean
Version history:
2.8.0 - added
params[poll][hide_totals]
Description: Whether the poll should hide total votes until after voting has ended.
Type: optional Boolean
Version history:
2.8.0 - added
params[media_ids]
Description: IDs of the MediaAttachments that will be attached to the status.
Type: nullable Array of String
Version history:
2.7.0 - added
params[sensitive]
Description: Whether the status will be marked as sensitive.
Type: nullable Boolean
Version history:
2.7.0 - added
params[spoiler_text]
Description: The text of the content warning or summary for the status.
Type: nullable String
Version history:
2.7.0 - added
params[visibility]
Description: The visibility that the status will have once it is posted.
Type: String (Enumerable oneOf)
public = Visible to everyone, shown in public timelines.
unlisted = Visible to public, but not included in public timelines.
private = Visible to followers only, and to any mentioned users.
direct = Visible only to mentioned users.
Version history:
2.7.0 - added
params[in_reply_to_id]
Description: ID of the Status that will be replied to.
Type: nullable Integer
Version history:
2.7.0 - added
params[language]
Description: The language that will be used for the status.
Type: nullable String (ISO 639-1 two-letter language code)
Version history:
2.7.0 - added
params[application_id] deprecated
Description: Internal ID of the Application that posted the status. Provided for historical compatibility only and can be ignored.
Type: Integer
Version history:
2.7.0 - added
params[scheduled_at]
Description: When the status will be scheduled. This will be null because the status is only scheduled once.
Type: nullable Null
Version history:
2.7.0 - added
params[idempotency]
Description: Idempotency key to prevent duplicate statuses.
Type: nullable String
Version history:
2.7.0 - added
params[with_rate_limit] deprecated
Description: Whether status creation is subject to rate limiting. Provided for historical compatibility only and can be ignored.
Type: Boolean
Version history:
2.7.0 - added
media_attachments
Description: Media that will be attached when the status is posted.
Type: Array of MediaAttachment
Version history:
2.7.0 - added
*/
export const ScheduledStatus = z.object({
id: Id.openapi({
description: "ID of the scheduled status in the database.",
example: "2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
}),
scheduled_at: z.string().datetime().openapi({
description: "When the status will be scheduled.",
example: "2025-01-07T14:11:00.000Z",
}),
media_attachments: Status.shape.media_attachments,
params: z.object({
text: z.string().openapi({
description: "Text to be used as status content.",
example: "Hello, world!",
}),
poll: Status.shape.poll,
media_ids: z
.array(Id)
.nullable()
.openapi({
description:
"IDs of the MediaAttachments that will be attached to the status.",
example: ["1234567890", "1234567891"],
}),
sensitive: Status.shape.sensitive,
spoiler_text: Status.shape.spoiler_text,
visibility: Status.shape.visibility,
in_reply_to_id: Status.shape.in_reply_to_id,
/** Versia Server API Extension */
quote_id: z.string().openapi({
description: "ID of the status being quoted.",
example: "c5d62a13-f340-4e7d-8942-7fd14be688dc",
}),
language: Status.shape.language,
scheduled_at: z.null().openapi({
description:
"When the status will be scheduled. This will be null because the status is only scheduled once.",
example: null,
}),
idempotency: z.string().nullable().openapi({
description: "Idempotency key to prevent duplicate statuses.",
example: "1234567890",
}),
}),
});

View file

@ -8,6 +8,7 @@ import type { Context } from "../schemas/context.ts";
import type { CustomEmoji } from "../schemas/emoji.ts"; import type { CustomEmoji } from "../schemas/emoji.ts";
import type { ExtendedDescription } from "../schemas/extended-description.ts"; import type { ExtendedDescription } from "../schemas/extended-description.ts";
import type { FamiliarFollowers } from "../schemas/familiar-followers.ts"; import type { FamiliarFollowers } from "../schemas/familiar-followers.ts";
import type { Filter, FilterKeyword } from "../schemas/filters.ts";
import type { Instance } from "../schemas/instance.ts"; import type { Instance } from "../schemas/instance.ts";
import type { Marker } from "../schemas/marker.ts"; import type { Marker } from "../schemas/marker.ts";
import type { Notification } from "../schemas/notification.ts"; import type { Notification } from "../schemas/notification.ts";
@ -17,8 +18,13 @@ import type { PrivacyPolicy } from "../schemas/privacy-policy.ts";
import type { WebPushSubscription } from "../schemas/pushsubscription.ts"; import type { WebPushSubscription } from "../schemas/pushsubscription.ts";
import type { Relationship } from "../schemas/relationship.ts"; import type { Relationship } from "../schemas/relationship.ts";
import type { Report } from "../schemas/report.ts"; import type { Report } from "../schemas/report.ts";
import type { Rule } from "../schemas/rule.ts";
import type { Search } from "../schemas/search.ts"; import type { Search } from "../schemas/search.ts";
import type { Status, StatusSource } from "../schemas/status.ts"; import type {
ScheduledStatus,
Status,
StatusSource,
} from "../schemas/status.ts";
import type { Tag } from "../schemas/tag.ts"; import type { Tag } from "../schemas/tag.ts";
import type { Token } from "../schemas/token.ts"; import type { Token } from "../schemas/token.ts";
import type { TermsOfService } from "../schemas/tos.ts"; import type { TermsOfService } from "../schemas/tos.ts";
@ -233,6 +239,41 @@ export class Client extends BaseClient {
return this.post<unknown>("/api/v1/featured_tags", { name }, extra); return this.post<unknown>("/api/v1/featured_tags", { name }, extra);
} }
/**
* POST /api/v2/filters
*
* Create a filter.
* @param context Filter context.
* @param title Filter title.
* @param filter_action Filter action.
* @param options.expires_in How many seconds from now should the filter expire?
* @param options.keywords_attributes Keywords to filter.
*/
public createFilter(
context: z.infer<typeof Filter.shape.context>,
title: z.infer<typeof Filter.shape.title>,
filter_action: z.infer<typeof Filter.shape.filter_action>,
options: Partial<{
expires_in: number;
keywords_attributes: {
keyword: z.infer<typeof FilterKeyword.shape.keyword>;
whole_word: z.infer<typeof FilterKeyword.shape.whole_word>;
}[];
}>,
extra?: RequestInit,
): Promise<Output<z.infer<typeof Filter>>> {
return this.post<z.infer<typeof Filter>>(
"/api/v2/filters",
{
context,
title,
filter_action,
...options,
},
extra,
);
}
// FIXME: No List schema // FIXME: No List schema
/** /**
* POST /api/v1/lists * POST /api/v1/lists
@ -350,6 +391,18 @@ export class Client extends BaseClient {
return this.delete(`/api/v1/featured_tags/${id}`, undefined, extra); return this.delete(`/api/v1/featured_tags/${id}`, undefined, extra);
} }
/**
* DELETE /api/v2/filters/:id
*
* @param id Target filter ID.
*/
public deleteFilter(
id: string,
extra?: RequestInit,
): Promise<Output<void>> {
return this.delete(`/api/v2/filters/${id}`, undefined, extra);
}
/** /**
* DELETE /api/v1/lists/:id * DELETE /api/v1/lists/:id
* *
@ -412,6 +465,28 @@ export class Client extends BaseClient {
); );
} }
/**
* DELETE /api/v1/notifications/destroy_multiple
*
* @param ids Target notification IDs.
*/
public dismissMultipleNotifications(
ids: string[],
extra?: RequestInit,
): Promise<Output<void>> {
const params = new URLSearchParams();
for (const id of ids) {
params.append("ids[]", id);
}
return this.delete(
`/api/v1/notifications/destroy_multiple?${params.toString()}`,
undefined,
extra,
);
}
/** /**
* POST /api/v1/notifications/:id/dismiss * POST /api/v1/notifications/:id/dismiss
* *
@ -1122,8 +1197,29 @@ export class Client extends BaseClient {
return this.get<unknown[]>("/api/v1/featured_tags", extra); return this.get<unknown[]>("/api/v1/featured_tags", extra);
} }
// TODO: getFilter /**
// TODO: getFilters * GET /api/v2/filters/:id
*
* @param id The filter ID.
* @return Filter.
*/
public getFilter(
id: string,
extra?: RequestInit,
): Promise<Output<z.infer<typeof Filter>>> {
return this.get<z.infer<typeof Filter>>(`/api/v2/filters/${id}`, extra);
}
/**
* GET /api/v2/filters
*
* @return Array of filters.
*/
public getFilters(
extra?: RequestInit,
): Promise<Output<z.infer<typeof Filter>[]>> {
return this.get<z.infer<typeof Filter>[]>("/api/v2/filters", extra);
}
/** /**
* GET /api/v1/follow_requests * GET /api/v1/follow_requests
@ -1207,7 +1303,7 @@ export class Client extends BaseClient {
} }
/** /**
* GET /api/v1/instance * GET /api/v2/instance
* *
* @return Instance. * @return Instance.
*/ */
@ -1492,7 +1588,7 @@ export class Client extends BaseClient {
const params = new URLSearchParams(); const params = new URLSearchParams();
for (const timeline of timelines) { for (const timeline of timelines) {
params.append("timelines[]", timeline); params.append("timeline[]", timeline);
} }
return this.get<z.infer<typeof Marker> | Record<never, never>>( return this.get<z.infer<typeof Marker> | Record<never, never>>(
@ -1659,6 +1755,8 @@ export class Client extends BaseClient {
since_id: string; since_id: string;
limit: number; limit: number;
only_media: boolean; only_media: boolean;
local: boolean;
remote: boolean;
}>, }>,
extra?: RequestInit, extra?: RequestInit,
): Promise<Output<z.infer<typeof Status>[]>> { ): Promise<Output<z.infer<typeof Status>[]>> {
@ -1680,6 +1778,12 @@ export class Client extends BaseClient {
if (options.only_media) { if (options.only_media) {
params.set("only_media", "true"); params.set("only_media", "true");
} }
if (options.local) {
params.set("local", "true");
}
if (options.remote) {
params.set("remote", "true");
}
} }
return this.get<z.infer<typeof Status>[]>( return this.get<z.infer<typeof Status>[]>(
@ -1780,7 +1884,20 @@ export class Client extends BaseClient {
return this.get<z.infer<typeof Role>[]>("/api/v1/roles", extra); return this.get<z.infer<typeof Role>[]>("/api/v1/roles", extra);
} }
// FIXME: No ScheduledStatus schema /**
* GET /api/v1/instance/rules
*
* @return Array of rules.
*/
public getRules(
extra?: RequestInit,
): Promise<Output<z.infer<typeof Rule>[]>> {
return this.get<z.infer<typeof Rule>[]>(
"/api/v1/instance/rules",
extra,
);
}
/** /**
* GET /api/v1/scheduled_statuses/:id * GET /api/v1/scheduled_statuses/:id
* *
@ -1790,11 +1907,13 @@ export class Client extends BaseClient {
public getScheduledStatus( public getScheduledStatus(
id: string, id: string,
extra?: RequestInit, extra?: RequestInit,
): Promise<Output<unknown>> { ): Promise<Output<z.infer<typeof ScheduledStatus>>> {
return this.get<unknown>(`/api/v1/scheduled_statuses/${id}`, extra); return this.get<z.infer<typeof ScheduledStatus>>(
`/api/v1/scheduled_statuses/${id}`,
extra,
);
} }
// FIXME: No ScheduledStatus schema
/** /**
* GET /api/v1/scheduled_statuses * GET /api/v1/scheduled_statuses
* *
@ -1812,7 +1931,7 @@ export class Client extends BaseClient {
limit: number; limit: number;
}>, }>,
extra?: RequestInit, extra?: RequestInit,
): Promise<Output<unknown[]>> { ): Promise<Output<z.infer<typeof ScheduledStatus>[]>> {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (options) { if (options) {
@ -1830,7 +1949,7 @@ export class Client extends BaseClient {
} }
} }
return this.get<unknown[]>( return this.get<z.infer<typeof ScheduledStatus>[]>(
`/api/v1/scheduled_statuses?${params}`, `/api/v1/scheduled_statuses?${params}`,
extra, extra,
); );
@ -2179,7 +2298,6 @@ export class Client extends BaseClient {
); );
} }
// FIXME: No ScheduledStatus schema
/** /**
* POST /api/v1/statuses * POST /api/v1/statuses
* *
@ -2199,7 +2317,7 @@ export class Client extends BaseClient {
*/ */
public postStatus( public postStatus(
status: string, status: string,
options: { options?: {
in_reply_to_id?: string; in_reply_to_id?: string;
quote_id?: string; quote_id?: string;
media_ids?: string[]; media_ids?: string[];
@ -2207,7 +2325,7 @@ export class Client extends BaseClient {
spoiler_text?: string; spoiler_text?: string;
visibility?: z.infer<typeof Status.shape.visibility>; visibility?: z.infer<typeof Status.shape.visibility>;
content_type?: StatusContentType; content_type?: StatusContentType;
scheduled_at?: string; scheduled_at?: Date;
language?: string; language?: string;
local_only?: boolean; local_only?: boolean;
poll?: { poll?: {
@ -2218,10 +2336,18 @@ export class Client extends BaseClient {
}; };
}, },
extra?: RequestInit, extra?: RequestInit,
): Promise<Output<z.infer<typeof Status> | unknown>> { ): Promise<
return this.post<z.infer<typeof Status> | unknown>( Output<z.infer<typeof Status> | z.infer<typeof ScheduledStatus>>
> {
return this.post<
z.infer<typeof Status> | z.infer<typeof ScheduledStatus>
>(
"/api/v1/statuses", "/api/v1/statuses",
{ status, ...options }, {
status,
...options,
scheduled_at: options?.scheduled_at?.toISOString(),
},
extra, extra,
); );
} }
@ -2455,15 +2581,31 @@ export class Client extends BaseClient {
}; };
}>, }>,
extra?: RequestInit, extra?: RequestInit,
): Promise<Output<z.infer<typeof Marker>>> { ): Promise<
return this.post<z.infer<typeof Marker>>( Output<{
"/api/v1/markers", home?: z.infer<typeof Marker>;
options, notifications?: z.infer<typeof Marker>;
extra, }>
> {
const params = new URLSearchParams();
if (options.home) {
params.set("home[last_read_id]", options.home.last_read_id);
}
if (options.notifications) {
params.set(
"notifications[last_read_id]",
options.notifications.last_read_id,
); );
} }
// FIXME: No ScheduledStatus schema return this.post<{
home?: z.infer<typeof Marker>;
notifications?: z.infer<typeof Marker>;
}>(`/api/v1/markers?${params}`, undefined, extra);
}
/** /**
* PUT /api/v1/scheduled_statuses/:id * PUT /api/v1/scheduled_statuses/:id
* *
@ -2475,8 +2617,8 @@ export class Client extends BaseClient {
id: string, id: string,
scheduled_at?: Date, scheduled_at?: Date,
extra?: RequestInit, extra?: RequestInit,
): Promise<Output<unknown>> { ): Promise<Output<z.infer<typeof ScheduledStatus>>> {
return this.put<unknown>( return this.put<z.infer<typeof ScheduledStatus>>(
`/api/v1/scheduled_statuses/${id}`, `/api/v1/scheduled_statuses/${id}`,
{ scheduled_at: scheduled_at?.toISOString() }, { scheduled_at: scheduled_at?.toISOString() },
extra, extra,
@ -2648,7 +2790,13 @@ export class Client extends BaseClient {
): Promise<Output<z.infer<typeof WebPushSubscription>>> { ): Promise<Output<z.infer<typeof WebPushSubscription>>> {
return this.post<z.infer<typeof WebPushSubscription>>( return this.post<z.infer<typeof WebPushSubscription>>(
"/api/v1/push/subscription", "/api/v1/push/subscription",
{ subscription, data }, {
subscription,
policy: data?.policy,
data: {
alerts: data?.alerts,
},
},
extra, extra,
); );
} }
@ -2902,7 +3050,40 @@ export class Client extends BaseClient {
); );
} }
// TODO: updateFilter /**
* PUT /api/v2/filters/:id
*
* @param id Target filter ID.
* @param options.title New filter title.
* @param options.context New filter context.
* @param options.filter_action New filter action.
* @param options.expires_in New filter expiration.
* @param options.keywords_attributes New filter keywords.
* @return Filter.
*/
public updateFilter(
id: string,
options: Partial<{
title: string;
context: string[];
filter_action: string;
expires_in: number;
keywords_attributes: Partial<{
id: string;
keyword: string;
whole_word: boolean;
_destroy: boolean;
}>[];
}>,
extra?: RequestInit,
): Promise<Output<z.infer<typeof Filter>>> {
return this.put<z.infer<typeof Filter>>(
`/api/v2/filters/${id}`,
options,
extra,
);
}
// FIXME: No List schema // FIXME: No List schema
/** /**
* PUT /api/v1/lists/:id * PUT /api/v1/lists/:id
@ -2988,7 +3169,7 @@ export class Client extends BaseClient {
): Promise<Output<z.infer<typeof WebPushSubscription>>> { ): Promise<Output<z.infer<typeof WebPushSubscription>>> {
return this.put<z.infer<typeof WebPushSubscription>>( return this.put<z.infer<typeof WebPushSubscription>>(
"/api/v1/push/subscription", "/api/v1/push/subscription",
{ ...data, policy }, { data, policy },
extra, extra,
); );
} }

View file

@ -38,6 +38,7 @@ export const generateClient = async (
): Promise< ): Promise<
VersiaClient & { VersiaClient & {
[Symbol.asyncDispose](): Promise<void>; [Symbol.asyncDispose](): Promise<void>;
dbToken: Token;
} }
> => { > => {
const token = user const token = user
@ -69,8 +70,12 @@ export const generateClient = async (
await token?.delete(); await token?.delete();
}; };
// @ts-expect-error More monkeypatching
client.dbToken = token;
return client as VersiaClient & { return client as VersiaClient & {
[Symbol.asyncDispose](): Promise<void>; [Symbol.asyncDispose](): Promise<void>;
dbToken: Token;
}; };
}; };
export const deleteOldTestUsers = async (): Promise<void> => { export const deleteOldTestUsers = async (): Promise<void> => {