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 { Role } from "@versia/kit/db";
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 higherPriorityRole: Role;
@ -44,56 +44,44 @@ afterAll(async () => {
// /api/v1/accounts/:id/roles/:role_id
describe("/api/v1/accounts/:id/roles/:role_id", () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(
`/api/v1/accounts/${users[1].id}/roles/${role.id}`,
{
method: "POST",
},
);
await using client = await generateClient();
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 () => {
const response = await fakeRequest(
`/api/v1/accounts/${users[1].id}/roles/00000000-0000-0000-0000-000000000000`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
await using client = await generateClient(users[0]);
const { ok, raw } = await client.assignRole(
users[1].id,
"00000000-0000-0000-0000-000000000000",
);
expect(response.status).toBe(404);
expect(ok).toBe(false);
expect(raw.status).toBe(404);
});
test("should return 404 if user does not exist", async () => {
const response = await fakeRequest(
`/api/v1/accounts/00000000-0000-0000-0000-000000000000/roles/${role.id}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
await using client = await generateClient(users[0]);
const { ok, raw } = await client.assignRole(
"00000000-0000-0000-0000-000000000000",
role.id,
);
expect(response.status).toBe(404);
expect(ok).toBe(false);
expect(raw.status).toBe(404);
});
test("should assign role to user", async () => {
const response = await fakeRequest(
`/api/v1/accounts/${users[1].id}/roles/${role.id}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
await using client = await generateClient(users[0]);
expect(response.status).toBe(204);
const { ok } = await client.assignRole(users[1].id, role.id);
expect(ok).toBe(true);
// Check if role was assigned
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 () => {
const response = await fakeRequest(
`/api/v1/accounts/${users[1].id}/roles/${higherPriorityRole.id}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
await using client = await generateClient(users[0]);
const { data, ok, raw } = await client.assignRole(
users[1].id,
higherPriorityRole.id,
);
expect(response.status).toBe(403);
const output = await response.json();
expect(output).toMatchObject({
expect(ok).toBe(false);
expect(raw.status).toBe(403);
expect(data).toMatchObject({
error: "Forbidden",
details:
"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 () => {
const response = await fakeRequest(
`/api/v1/accounts/${users[1].id}/roles/${role.id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
await using client = await generateClient(users[0]);
expect(response.status).toBe(204);
const { ok } = await client.unassignRole(users[1].id, role.id);
expect(ok).toBe(true);
// Check if role was removed
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 () => {
await higherPriorityRole.linkUser(users[1].id);
const response = await fakeRequest(
`/api/v1/accounts/${users[1].id}/roles/${higherPriorityRole.id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
await using client = await generateClient(users[0]);
const { data, ok, raw } = await client.unassignRole(
users[1].id,
higherPriorityRole.id,
);
expect(response.status).toBe(403);
const output = await response.json();
expect(output).toMatchObject({
expect(ok).toBe(false);
expect(raw.status).toBe(403);
expect(data).toMatchObject({
error: "Forbidden",
details:
"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 { Role } from "@versia/kit/db";
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;
beforeAll(async () => {
@ -29,37 +29,27 @@ afterAll(async () => {
describe("/api/v1/accounts/:id/roles", () => {
test("should return 404 if user does not exist", async () => {
const response = await fakeRequest(
"/api/v1/accounts/00000000-0000-0000-0000-000000000000/roles",
{
method: "GET",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
await using client = await generateClient(users[0]);
const { data, ok, raw } = await client.getAccountRoles(
"00000000-0000-0000-0000-000000000000",
);
expect(response.status).toBe(404);
const output = await response.json();
expect(output).toMatchObject({
expect(ok).toBe(false);
expect(raw.status).toBe(404);
expect(data).toMatchObject({
error: "User not found",
});
});
test("should return a list of roles for the user", async () => {
const response = await fakeRequest(
`/api/v1/accounts/${users[0].id}/roles`,
{
method: "GET",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
await using client = await generateClient(users[0]);
expect(response.ok).toBe(true);
const roles = await response.json();
expect(roles).toContainEqual({
const { data, ok } = await client.getAccountRoles(users[0].id);
expect(ok).toBe(true);
expect(data).toBeArray();
expect(data).toContainEqual({
id: role.id,
name: "test",
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 { Emojis } from "@versia/kit/tables";
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 = "";
// Make user 2 an admin
@ -12,22 +12,14 @@ beforeAll(async () => {
await users[1].update({ isAdmin: true });
// Create an emoji
const response = await fakeRequest("/api/v1/emojis", {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
shortcode: "test",
element: "https://cdn.versia.social/logo.webp",
global: true,
}),
});
await using client = await generateClient(users[1]);
const { data, ok } = await client.uploadEmoji(
"test",
new URL("https://cdn.versia.social/logo.webp"),
);
expect(response.ok).toBe(true);
const emoji = await response.json();
id = emoji.id;
expect(ok).toBe(true);
id = data.id;
});
afterAll(async () => {
@ -41,124 +33,95 @@ afterAll(async () => {
// /api/v1/emojis/:id (PATCH, DELETE, GET)
describe("/api/v1/emojis/:id", () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(`/api/v1/emojis/${id}`, {
method: "GET",
});
await using client = await generateClient();
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 () => {
const response = await fakeRequest(
"/api/v1/emojis/00000000-0000-0000-0000-000000000000",
{
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
method: "GET",
},
await using client = await generateClient(users[1]);
const { ok, raw } = await client.getEmoji(
"00000000-0000-0000-0000-000000000000",
);
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 () => {
const response = await fakeRequest(`/api/v1/emojis/${id}`, {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
method: "PATCH",
body: JSON.stringify({
await using client = await generateClient(users[0]);
const { ok, raw } = await client.updateEmoji(id, {
shortcode: "test2",
}),
});
expect(response.status).toBe(403);
expect(ok).toBe(false);
expect(raw.status).toBe(403);
});
test("should return the emoji", async () => {
const response = await fakeRequest(`/api/v1/emojis/${id}`, {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
method: "GET",
});
await using client = await generateClient(users[1]);
expect(response.ok).toBe(true);
const emoji = await response.json();
expect(emoji.shortcode).toBe("test");
const { data, ok } = await client.getEmoji(id);
expect(ok).toBe(true);
expect(data.shortcode).toBe("test");
});
test("should update the emoji", async () => {
const response = await fakeRequest(`/api/v1/emojis/${id}`, {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
method: "PATCH",
body: JSON.stringify({
await using client = await generateClient(users[1]);
const { data, ok } = await client.updateEmoji(id, {
shortcode: "test2",
}),
});
expect(response.ok).toBe(true);
const emoji = await response.json();
expect(emoji.shortcode).toBe("test2");
expect(ok).toBe(true);
expect(data.shortcode).toBe("test2");
});
test("should update the emoji with another url, but keep the shortcode", async () => {
const response = await fakeRequest(`/api/v1/emojis/${id}`, {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
method: "PATCH",
body: JSON.stringify({
element: "https://avatars.githubusercontent.com/u/30842467?v=4",
}),
await using client = await generateClient(users[1]);
const { data, ok } = await client.updateEmoji(id, {
image: new URL(
"https://avatars.githubusercontent.com/u/30842467?v=4",
),
});
expect(response.ok).toBe(true);
const emoji = await response.json();
expect(emoji.shortcode).toBe("test2");
expect(ok).toBe(true);
expect(data.shortcode).toBe("test2");
expect(data.url).toContain("/media/proxy/");
});
test("should update the emoji to be non-global", async () => {
const response = await fakeRequest(`/api/v1/emojis/${id}`, {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
method: "PATCH",
body: JSON.stringify({
await using client = await generateClient(users[1]);
const { data, ok } = await client.updateEmoji(id, {
global: false,
}),
});
expect(response.ok).toBe(true);
expect(ok).toBe(true);
expect(data.global).toBe(false);
// Check if the other user can see it
const response2 = await fakeRequest("/api/v1/custom_emojis", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
method: "GET",
});
await using client2 = await generateClient(users[0]);
expect(response2.ok).toBe(true);
const emojis = await response2.json();
expect(emojis).not.toContainEqual(expect.objectContaining({ id }));
const { data: data2, ok: ok2 } =
await client2.getInstanceCustomEmojis();
expect(ok2).toBe(true);
expect(data2).not.toContainEqual(expect.objectContaining({ id }));
});
test("should delete the emoji", async () => {
const response = await fakeRequest(`/api/v1/emojis/${id}`, {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
method: "DELETE",
});
await using client = await generateClient(users[1]);
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 { inArray } from "drizzle-orm";
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
beforeAll(async () => {
@ -39,146 +39,109 @@ const createImage = async (name: string): Promise<File> => {
describe("/api/v1/emojis", () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest("/api/v1/emojis", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
shortcode: "test",
element: "https://cdn.versia.social/logo.webp",
}),
});
await using client = await generateClient();
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", () => {
test("should upload a file and create an emoji", async () => {
const formData = new FormData();
formData.append("shortcode", "test1");
formData.append("element", await createImage("test.png"));
formData.append("global", "true");
await using client = await generateClient(users[1]);
const response = await fakeRequest("/api/v1/emojis", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
const { data, ok } = await client.uploadEmoji(
"test1",
await createImage("test.png"),
{
global: true,
},
body: formData,
});
);
expect(response.ok).toBe(true);
const emoji = await response.json();
expect(emoji.shortcode).toBe("test1");
expect(emoji.url).toContain("/media/proxy");
expect(ok).toBe(true);
expect(data.shortcode).toBe("test1");
expect(data.url).toContain("/media/proxy");
});
test("should try to upload a non-image", async () => {
const formData = new FormData();
formData.append("shortcode", "test2");
formData.append("element", new File(["test"], "test.txt"));
await using client = await generateClient(users[1]);
const response = await fakeRequest("/api/v1/emojis", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
body: formData,
});
const { ok, raw } = await client.uploadEmoji(
"test2",
new File(["test"], "test.txt"),
);
expect(response.status).toBe(422);
expect(ok).toBe(false);
expect(raw.status).toBe(422);
});
test("should upload an emoji by url", async () => {
const response = await fakeRequest("/api/v1/emojis", {
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",
}),
});
await using client = await generateClient(users[1]);
expect(response.ok).toBe(true);
const emoji = await response.json();
expect(emoji.shortcode).toBe("test3");
expect(emoji.url).toContain("/media/proxy/");
const { data, ok } = await client.uploadEmoji(
"test3",
new URL("https://cdn.versia.social/logo.webp"),
);
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 () => {
const formData = new FormData();
formData.append("shortcode", "test1");
formData.append("element", await createImage("test-image.png"));
await using client = await generateClient(users[1]);
const response = await fakeRequest("/api/v1/emojis", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
body: formData,
});
const { ok, raw } = await client.uploadEmoji(
"test1",
await createImage("test-image.png"),
);
expect(response.status).toBe(422);
expect(ok).toBe(false);
expect(raw.status).toBe(422);
});
});
describe("User tests", () => {
test("should upload a file and create an emoji", async () => {
const formData = new FormData();
formData.append("shortcode", "test4");
formData.append("element", await createImage("test-image.png"));
await using client = await generateClient(users[0]);
const response = await fakeRequest("/api/v1/emojis", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: formData,
});
const { data, ok } = await client.uploadEmoji(
"test4",
await createImage("test-image.png"),
);
expect(response.ok).toBe(true);
const emoji = await response.json();
expect(emoji.shortcode).toBe("test4");
expect(emoji.url).toContain("/media/proxy/");
expect(ok).toBe(true);
expect(data.shortcode).toBe("test4");
expect(data.url).toContain("/media/proxy");
});
test("should fail when uploading an already existing global emoji", async () => {
const formData = new FormData();
formData.append("shortcode", "test1");
formData.append("element", await createImage("test-image.png"));
await using client = await generateClient(users[0]);
const response = await fakeRequest("/api/v1/emojis", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: formData,
});
const { ok, raw } = await client.uploadEmoji(
"test1",
await createImage("test-image.png"),
);
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 () => {
const formData = new FormData();
formData.append("shortcode", "test4");
formData.append("element", await createImage("test-image.png"));
await using client = await generateClient(users[2]);
const response = await fakeRequest("/api/v1/emojis", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[2].data.accessToken}`,
},
body: formData,
});
const { data, ok } = await client.uploadEmoji(
"test4",
await createImage("test-image.png"),
);
expect(response.ok).toBe(true);
const emoji = await response.json();
expect(emoji.shortcode).toBe("test4");
expect(emoji.url).toContain("/media/proxy/");
expect(ok).toBe(true);
expect(data.shortcode).toBe("test4");
expect(data.url).toContain("/media/proxy/");
});
});
});

View file

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

View file

@ -1,16 +1,16 @@
import { describe, expect, test } from "bun:test";
import { config } from "~/config.ts";
import { fakeRequest } from "~/tests/utils";
import { generateClient } from "~/tests/utils";
// /api/v1/instance/rules
describe("/api/v1/instance/rules", () => {
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(json).toEqual(
expect(ok).toBe(true);
expect(data).toEqual(
config.instance.rules.map((r, index) => ({
id: String(index),
text: r.text,

View file

@ -1,15 +1,15 @@
import { describe, expect, test } from "bun:test";
import { fakeRequest } from "~/tests/utils";
import { generateClient } from "~/tests/utils";
// /api/v1/instance/terms_of_service
describe("/api/v1/instance/terms_of_service", () => {
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(json).toEqual({
expect(ok).toBe(true);
expect(data).toEqual({
updated_at: new Date(0).toISOString(),
// This instance has not provided any terms of service.
content:

View file

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

View file

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

View file

@ -1,28 +1,24 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Notification as ApiNotification } from "@versia/client/types";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import type { z } from "@hono/zod-openapi";
import type { Notification } from "@versia/client-ng/schemas";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(2);
let notifications: ApiNotification[] = [];
const { users, deleteUsers } = await getTestUsers(2);
let notifications: z.infer<typeof Notification>[] = [];
// Create some test notifications: follow, favourite, reblog, mention
beforeAll(async () => {
await fakeRequest(`/api/v1/accounts/${users[0].id}/follow`, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
await using client0 = await generateClient(users[0]);
await using client1 = await generateClient(users[1]);
notifications = await fakeRequest("/api/v1/notifications", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
}).then((r) => r.json());
const { ok } = await client1.followAccount(users[0].id);
expect(notifications.length).toBe(1);
expect(ok).toBe(true);
const { data } = await client0.getNotifications();
expect(data).toBeArrayOfSize(1);
notifications = data;
});
afterAll(async () => {
@ -31,55 +27,41 @@ afterAll(async () => {
describe("/api/v1/notifications/:id/dismiss", () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(
`/api/v1/notifications/${notifications[0].id}/dismiss`,
{
method: "POST",
},
await using client = await generateClient();
const { ok, raw } = await client.dismissNotification(
notifications[0].id,
);
expect(response.status).toBe(401);
expect(ok).toBe(false);
expect(raw.status).toBe(401);
});
test("should dismiss notification", async () => {
const response = await fakeRequest(
`/api/v1/notifications/${notifications[0].id}/dismiss`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
await using client = await generateClient(users[0]);
expect(response.status).toBe(200);
const { ok } = await client.dismissNotification(notifications[0].id);
expect(ok).toBe(true);
});
test("should not display dismissed notification", async () => {
const response = await fakeRequest("/api/v1/notifications", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
await using client = await generateClient(users[0]);
expect(response.status).toBe(200);
const { data, ok } = await client.getNotifications();
const output = await response.json();
expect(output.length).toBe(0);
expect(ok).toBe(true);
expect(data).toBeArrayOfSize(0);
});
test("should not be able to dismiss other user's notifications", async () => {
const response = await fakeRequest(
`/api/v1/notifications/${notifications[0].id}/dismiss`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
},
await using client = await generateClient(users[1]);
const { ok, raw } = await client.dismissNotification(
notifications[0].id,
);
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 type { Notification as ApiNotification } from "@versia/client/types";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import type { z } from "@hono/zod-openapi";
import type { Notification } from "@versia/client-ng/schemas";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(2);
let notifications: ApiNotification[] = [];
const { users, deleteUsers } = await getTestUsers(2);
let notifications: z.infer<typeof Notification>[] = [];
// Create some test notifications: follow, favourite, reblog, mention
beforeAll(async () => {
await fakeRequest(`/api/v1/accounts/${users[0].id}/follow`, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
await using client0 = await generateClient(users[0]);
await using client1 = await generateClient(users[1]);
notifications = await fakeRequest("/api/v1/notifications", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
}).then((r) => r.json());
const { ok } = await client1.followAccount(users[0].id);
expect(notifications.length).toBe(1);
expect(ok).toBe(true);
const { data } = await client0.getNotifications();
expect(data).toBeArrayOfSize(1);
notifications = data;
});
afterAll(async () => {
@ -32,66 +27,54 @@ afterAll(async () => {
// /api/v1/notifications/:id
describe("/api/v1/notifications/:id", () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(
"/api/v1/notifications/00000000-0000-0000-0000-000000000000",
);
await using client = await generateClient();
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 () => {
const response = await fakeRequest("/api/v1/notifications/invalid", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
expect(response.status).toBe(422);
await using client = await generateClient(users[0]);
const { ok, raw } = await client.getNotification("invalid");
expect(ok).toBe(false);
expect(raw.status).toBe(422);
});
test("should return 404 if notification not found", async () => {
const response = await fakeRequest(
"/api/v1/notifications/00000000-0000-0000-0000-000000000000",
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
await using client = await generateClient(users[0]);
const { ok, raw } = await client.getNotification(
"00000000-0000-0000-0000-000000000000",
);
expect(response.status).toBe(404);
expect(ok).toBe(false);
expect(raw.status).toBe(404);
});
test("should return notification", async () => {
const response = await fakeRequest(
`/api/v1/notifications/${notifications[0].id}`,
{
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
await using client = await generateClient(users[0]);
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(notification.id).toBe(notifications[0].id);
expect(notification.type).toBe("follow");
expect(notification.account).toBeDefined();
expect(notification.account?.id).toBe(users[1].id);
expect(data).toBeDefined();
expect(data.id).toBe(notifications[0].id);
expect(data.type).toBe("follow");
expect(data.account).toBeDefined();
expect(data.account?.id).toBe(users[1].id);
});
test("should not be able to view other user's notifications", async () => {
const response = await fakeRequest(
`/api/v1/notifications/${notifications[0].id}`,
{
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
},
);
await using client = await generateClient(users[1]);
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 type { Notification as ApiNotification } from "@versia/client/types";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(2);
let notifications: ApiNotification[] = [];
const { users, deleteUsers } = await getTestUsers(2);
// Create some test notifications: follow, favourite, reblog, mention
beforeAll(async () => {
await fakeRequest(`/api/v1/accounts/${users[0].id}/follow`, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
await using client1 = await generateClient(users[1]);
await using client0 = await generateClient(users[0]);
notifications = await fakeRequest("/api/v1/notifications", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
}).then((r) => r.json());
const { ok } = await client1.followAccount(users[0].id);
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 () => {
@ -32,29 +25,23 @@ afterAll(async () => {
// /api/v1/notifications/clear
describe("/api/v1/notifications/clear", () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest("/api/v1/notifications/clear", {
method: "POST",
});
await using client = await generateClient();
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 () => {
const response = await fakeRequest("/api/v1/notifications/clear", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
await using client = await generateClient(users[0]);
expect(response.status).toBe(200);
const { ok } = await client.dismissNotifications();
const newNotifications = await fakeRequest("/api/v1/notifications", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
}).then((r) => r.json());
expect(ok).toBe(true);
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 type { Notification as ApiNotification } from "@versia/client/types";
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils";
import type { z } from "@hono/zod-openapi";
import type { Notification } from "@versia/client-ng/schemas";
import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(2);
const statuses = await getTestStatuses(40, users[0]);
let notifications: ApiNotification[] = [];
const { users, deleteUsers } = await getTestUsers(2);
const statuses = await getTestStatuses(5, users[0]);
let notifications: z.infer<typeof Notification>[] = [];
// Create some test notifications
beforeAll(async () => {
await fakeRequest(`/api/v1/accounts/${users[0].id}/follow`, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
await using client0 = await generateClient(users[0]);
await using client1 = await generateClient(users[1]);
const { ok } = await client1.followAccount(users[0].id);
expect(ok).toBe(true);
for (const i of [0, 1, 2, 3]) {
await fakeRequest(`/api/v1/statuses/${statuses[i].id}/favourite`, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
await client1.favouriteStatus(statuses[i].id);
}
notifications = await fakeRequest("/api/v1/notifications", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
}).then((r) => r.json());
const { data } = await client0.getNotifications();
expect(notifications.length).toBe(5);
expect(data).toBeArrayOfSize(5);
notifications = data;
});
afterAll(async () => {
@ -44,45 +33,33 @@ afterAll(async () => {
// /api/v1/notifications/destroy_multiple
describe("/api/v1/notifications/destroy_multiple", () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(
`/api/v1/notifications/destroy_multiple?${new URLSearchParams(
notifications.slice(1).map((n) => ["ids[]", n.id]),
).toString()}`,
{
method: "DELETE",
},
await using client = await generateClient();
const { ok, raw } = await client.dismissMultipleNotifications(
notifications.slice(1).map((n) => n.id),
);
expect(response.status).toBe(401);
expect(ok).toBe(false);
expect(raw.status).toBe(401);
});
test("should dismiss notifications", async () => {
const response = await fakeRequest(
`/api/v1/notifications/destroy_multiple?${new URLSearchParams(
notifications.slice(1).map((n) => ["ids[]", n.id]),
).toString()}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
await using client = await generateClient(users[0]);
const { ok, raw } = await client.dismissMultipleNotifications(
notifications.slice(1).map((n) => n.id),
);
expect(response.status).toBe(200);
expect(ok).toBe(true);
expect(raw.status).toBe(200);
});
test("should not display dismissed notification", async () => {
const response = await fakeRequest("/api/v1/notifications", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
test("should not display dismissed notifications", async () => {
await using client = await generateClient(users[0]);
expect(response.status).toBe(200);
const { data } = await client.getNotifications();
const output = await response.json();
expect(output.length).toBe(1);
expect(data).toBeArrayOfSize(1);
expect(data[0].id).toBe(notifications[0].id);
});
});

View file

@ -1,70 +1,33 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Notification as ApiNotification } from "@versia/client/types";
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils";
import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
const getFormData = (
object: Record<string, string | number | boolean>,
): 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();
const { users, deleteUsers } = await getTestUsers(2);
const timeline = (await getTestStatuses(5, users[0])).toReversed();
// Create some test notifications: follow, favourite, reblog, mention
beforeAll(async () => {
const res1 = await fakeRequest(`/api/v1/accounts/${users[0].id}/follow`, {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
await using client = await generateClient(users[1]);
expect(res1.status).toBe(200);
const { ok } = await client.followAccount(users[0].id);
const res2 = await fakeRequest(
`/api/v1/statuses/${timeline[0].id}/favourite`,
expect(ok).toBe(true);
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",
local_only: "true",
}),
});
local_only: true,
},
);
expect(res4.status).toBe(200);
expect(ok4).toBe(true);
});
afterAll(async () => {
@ -74,27 +37,22 @@ afterAll(async () => {
// /api/v1/notifications
describe("/api/v1/notifications", () => {
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 () => {
const response = await fakeRequest("/api/v1/notifications", {
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
await using client = await generateClient(users[0]);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
);
const { data, ok } = await client.getNotifications();
const objects = (await response.json()) as ApiNotification[];
expect(objects.length).toBe(4);
for (const [index, notification] of objects.entries()) {
expect(ok).toBe(true);
expect(data.length).toBe(4);
for (const [index, notification] of data.entries()) {
expect(notification.account).toBeDefined();
expect(notification.account?.id).toBe(users[1].id);
expect(notification.created_at).toBeDefined();
@ -103,64 +61,43 @@ describe("/api/v1/notifications", () => {
expect(notification.type).toBe(
["follow", "favourite", "reblog", "mention"].toReversed()[
index
],
] as "follow" | "favourite" | "reblog" | "mention",
);
}
});
test("should not return notifications with filtered keywords", async () => {
const filterResponse = await fakeRequest("/api/v2/filters", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/x-www-form-urlencoded",
await using client = await generateClient(users[0]);
const { data: filter, ok } = await client.createFilter(
["notifications"],
"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
expect(objects).not.toContainEqual(
expect(notifications).not.toContainEqual(
expect.objectContaining({
status: expect.objectContaining({ id: timeline[0].id }),
}),
);
// Delete filter
const filterDeleteResponse = await fakeRequest(
`/api/v2/filters/${(await filterResponse.json()).id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
);
const { ok: ok2 } = await client.deleteFilter(filter.id);
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 { PushSubscription } from "@versia/kit/db";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(2);
@ -15,42 +15,34 @@ beforeEach(async () => {
describe("/api/v1/push/subscriptions", () => {
test("should create a push subscription", async () => {
const res = await fakeRequest("/api/v1/push/subscription", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
alerts: {
update: true,
},
},
policy: "all",
subscription: {
await using client = await generateClient(users[0]);
const { ok, data } = await client.subscribePushNotifications(
{
endpoint: "https://example.com",
keys: {
auth: "test",
p256dh: "test",
auth: "testthatis24charactersha",
},
},
}),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body).toMatchObject({
endpoint: "https://example.com",
{
alerts: {
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 () => {
await using client = await generateClient(users[1]);
await PushSubscription.insert({
endpoint: "https://example.com",
alerts: {
@ -68,28 +60,21 @@ describe("/api/v1/push/subscriptions", () => {
policy: "all",
authSecret: "test",
publicKey: "test",
tokenId: tokens[1].id,
tokenId: client.dbToken.id,
});
const res = await fakeRequest("/api/v1/push/subscription", {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
});
const { ok, data } = await client.getPushSubscription();
expect(res.status).toBe(200);
const body = await res.json();
expect(body).toMatchObject({
endpoint: "https://example.com",
alerts: {
expect(ok).toBe(true);
expect(data.endpoint).toBe("https://example.com");
expect(data.alerts).toMatchObject({
update: true,
},
});
});
test("should update a push subscription", async () => {
await using client = await generateClient(users[0]);
await PushSubscription.insert({
endpoint: "https://example.com",
alerts: {
@ -107,65 +92,49 @@ describe("/api/v1/push/subscriptions", () => {
policy: "all",
authSecret: "test",
publicKey: "test",
tokenId: tokens[0].id,
tokenId: client.dbToken.id,
});
const res = await fakeRequest("/api/v1/push/subscription", {
method: "PUT",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
const { ok, data } = await client.updatePushSubscription(
{
alerts: {
update: false,
favourite: true,
},
},
policy: "follower",
}),
});
"follower",
);
expect(res.status).toBe(200);
expect(await res.json()).toMatchObject({
alerts: {
expect(ok).toBe(true);
expect(data.alerts).toMatchObject({
update: false,
favourite: true,
},
});
});
describe("permissions", () => {
test("should not allow watching admin reports without permissions", async () => {
const res = await fakeRequest("/api/v1/push/subscription", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
await using client = await generateClient(users[0]);
const { data, ok } = await client.subscribePushNotifications(
{
endpoint: "https://example.com",
keys: {
auth: "testthatis24charactersha",
p256dh: "test",
},
body: JSON.stringify({
data: {
},
{
alerts: {
"admin.report": true,
},
},
policy: "all",
subscription: {
endpoint: "https://example.com",
keys: {
p256dh: "test",
auth: "testthatis24charactersha",
},
},
}),
});
);
expect(res.status).toBe(200);
expect(await res.json()).toMatchObject({
alerts: {
expect(ok).toBe(true);
expect(data.alerts).toMatchObject({
"admin.report": false,
},
});
});
@ -174,30 +143,28 @@ describe("/api/v1/push/subscriptions", () => {
isAdmin: true,
});
const res = await fakeRequest("/api/v1/push/subscription", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
await using client = await generateClient(users[0]);
const { ok, data } = await client.subscribePushNotifications(
{
endpoint: "https://example.com",
keys: {
auth: "testthatis24charactersha",
p256dh: "test",
},
body: JSON.stringify({
data: {
},
{
alerts: {
"admin.report": true,
},
},
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({
isAdmin: false,
@ -205,6 +172,8 @@ describe("/api/v1/push/subscriptions", () => {
});
test("should not allow editing to add admin reports without permissions", async () => {
await using client = await generateClient(users[0]);
await PushSubscription.insert({
endpoint: "https://example.com",
alerts: {
@ -222,30 +191,21 @@ describe("/api/v1/push/subscriptions", () => {
policy: "all",
authSecret: "test",
publicKey: "test",
tokenId: tokens[0].id,
tokenId: client.dbToken.id,
});
const res = await fakeRequest("/api/v1/push/subscription", {
method: "PUT",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
const { ok, data } = await client.updatePushSubscription(
{
alerts: {
"admin.report": true,
},
},
policy: "all",
}),
});
"all",
);
expect(res.status).toBe(200);
expect(await res.json()).toMatchObject({
alerts: {
expect(ok).toBe(true);
expect(data.alerts).toMatchObject({
"admin.report": false,
},
});
});
@ -254,6 +214,8 @@ describe("/api/v1/push/subscriptions", () => {
isAdmin: true,
});
await using client = await generateClient(users[0]);
await PushSubscription.insert({
endpoint: "https://example.com",
alerts: {
@ -271,28 +233,20 @@ describe("/api/v1/push/subscriptions", () => {
policy: "all",
authSecret: "test",
publicKey: "test",
tokenId: tokens[0].id,
tokenId: client.dbToken.id,
});
const res = await fakeRequest("/api/v1/push/subscription", {
method: "PUT",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
const { ok, data } = await client.updatePushSubscription(
{
alerts: {
"admin.report": true,
},
},
policy: "all",
}),
});
"all",
);
expect(res.status).toBe(200);
expect(await res.json()).toMatchObject({
alerts: {
expect(ok).toBe(true);
expect(data.alerts).toMatchObject({
update: true,
"admin.report": true,
"admin.sign_up": false,
@ -303,7 +257,6 @@ describe("/api/v1/push/subscriptions", () => {
poll: false,
reblog: false,
status: false,
},
});
await users[0].update({

View file

@ -1,9 +1,9 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { Role } from "@versia/kit/db";
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 higherPriorityRole: Role;
@ -44,38 +44,32 @@ afterAll(async () => {
// /api/v1/roles/:id
describe("/api/v1/roles/:id", () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(`/api/v1/roles/${role.id}`, {
method: "GET",
});
await using client = await generateClient();
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 () => {
const response = await fakeRequest(
"/api/v1/roles/00000000-0000-0000-0000-000000000000",
{
method: "GET",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
await using client = await generateClient(users[0]);
const { ok, raw } = await client.getRole(
"00000000-0000-0000-0000-000000000000",
);
expect(response.status).toBe(404);
expect(ok).toBe(false);
expect(raw.status).toBe(404);
});
test("should return role data", async () => {
const response = await fakeRequest(`/api/v1/roles/${role.id}`, {
method: "GET",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
await using client = await generateClient(users[0]);
expect(response.status).toBe(200);
const responseData = await response.json();
expect(responseData).toMatchObject({
const { ok, data } = await client.getRole(role.id);
expect(ok).toBe(true);
expect(data).toMatchObject({
id: role.id,
name: role.data.name,
permissions: role.data.permissions,
@ -87,63 +81,56 @@ describe("/api/v1/roles/:id", () => {
});
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(`/api/v1/roles/${role.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "updatedName" }),
await using client = await generateClient();
const { ok, raw } = await client.updateRole(role.id, {
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 () => {
const response = await fakeRequest(
"/api/v1/roles/00000000-0000-0000-0000-000000000000",
await using client = await generateClient(users[0]);
const { ok, raw } = await client.updateRole(
"00000000-0000-0000-0000-000000000000",
{
method: "PATCH",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "updatedName" }),
name: "updatedName",
},
);
expect(response.status).toBe(404);
expect(ok).toBe(false);
expect(raw.status).toBe(404);
});
test("should update role data", async () => {
const response = await fakeRequest(`/api/v1/roles/${role.id}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "updatedName" }),
await using client = await generateClient(users[0]);
const { ok } = await client.updateRole(role.id, {
name: "updatedName",
});
expect(response.status).toBe(204);
expect(ok).toBe(true);
const updatedRole = await Role.fromId(role.id);
expect(updatedRole?.data.name).toBe("updatedName");
});
test("should return 403 if user tries to update role with higher priority", async () => {
const response = await fakeRequest(
`/api/v1/roles/${higherPriorityRole.id}`,
await using client = await generateClient(users[0]);
const { data, ok, raw } = await client.updateRole(
higherPriorityRole.id,
{
method: "PATCH",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "updatedName" }),
name: "updatedName",
},
);
expect(response.status).toBe(403);
const output = await response.json();
expect(output).toMatchObject({
expect(ok).toBe(false);
expect(raw.status).toBe(403);
expect(data).toMatchObject({
error: "Forbidden",
details:
"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 () => {
const response = await fakeRequest(`/api/v1/roles/${role.id}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
await using client = await generateClient(users[0]);
const { data, ok, raw } = await client.updateRole(role.id, {
permissions: [RolePermissions.Impersonate],
}),
});
expect(response.status).toBe(403);
const output = await response.json();
expect(output).toMatchObject({
expect(ok).toBe(false);
expect(raw.status).toBe(403);
expect(data).toMatchObject({
error: "Forbidden",
details: "User cannot add or remove permissions they do not have",
});
});
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest(`/api/v1/roles/${role.id}`, {
method: "DELETE",
});
await using client = await generateClient();
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 () => {
const response = await fakeRequest(
"/api/v1/roles/00000000-0000-0000-0000-000000000000",
{
method: "DELETE",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
await using client = await generateClient(users[0]);
const { ok, raw } = await client.deleteRole(
"00000000-0000-0000-0000-000000000000",
);
expect(response.status).toBe(404);
expect(ok).toBe(false);
expect(raw.status).toBe(404);
});
test("should delete role", async () => {
@ -202,33 +182,26 @@ describe("/api/v1/roles/:id", () => {
icon: "test",
});
const response = await fakeRequest(`/api/v1/roles/${newRole.id}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
await using client = await generateClient(users[0]);
expect(response.status).toBe(204);
const { ok } = await client.deleteRole(newRole.id);
expect(ok).toBe(true);
const deletedRole = await Role.fromId(newRole.id);
expect(deletedRole).toBeNull();
});
test("should return 403 if user tries to delete role with higher priority", async () => {
const response = await fakeRequest(
`/api/v1/roles/${higherPriorityRole.id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
},
await using client = await generateClient(users[0]);
const { data, ok, raw } = await client.deleteRole(
higherPriorityRole.id,
);
expect(response.status).toBe(403);
const output = await response.json();
expect(output).toMatchObject({
expect(ok).toBe(false);
expect(raw.status).toBe(403);
expect(data).toMatchObject({
error: "Forbidden",
details:
"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 { RolePermissions } from "@versia/kit/tables";
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;
beforeAll(async () => {
@ -32,24 +32,21 @@ afterAll(async () => {
// /api/v1/roles
describe("/api/v1/roles", () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest("/api/v1/roles", {
method: "GET",
});
await using client = await generateClient();
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 () => {
const response = await fakeRequest("/api/v1/roles", {
method: "GET",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
});
await using client = await generateClient(users[0]);
expect(response.ok).toBe(true);
const roles = await response.json();
expect(roles).toContainEqual({
const { ok, data } = await client.getRoles();
expect(ok).toBe(true);
expect(data).toContainEqual({
name: "test",
permissions: [RolePermissions.ManageRoles],
priority: 10,
@ -59,7 +56,7 @@ describe("/api/v1/roles", () => {
id: role.id,
});
expect(roles).toContainEqual({
expect(data).toContainEqual({
id: "default",
name: "Default",
permissions: config.permissions.default,
@ -70,25 +67,18 @@ describe("/api/v1/roles", () => {
});
test("should create a new role", async () => {
const response = await fakeRequest("/api/v1/roles", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "newRole",
await using client = await generateClient(users[0]);
const { ok, data } = await client.createRole("newRole", {
permissions: [RolePermissions.ManageRoles],
priority: 1,
description: "newRole",
visible: true,
icon: "https://example.com/icon.png",
}),
});
expect(response.ok).toBe(true);
const newRole = await response.json();
expect(newRole).toMatchObject({
expect(ok).toBe(true);
expect(data).toMatchObject({
name: "newRole",
permissions: [RolePermissions.ManageRoles],
priority: 1,
@ -98,7 +88,7 @@ describe("/api/v1/roles", () => {
});
// Cleanup
const createdRole = await Role.fromId(newRole.id);
const createdRole = await Role.fromId(data.id);
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 () => {
const response = await fakeRequest("/api/v1/roles", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "newRole",
await using client = await generateClient(users[0]);
const { data, ok, raw } = await client.createRole("newRole", {
permissions: [RolePermissions.ManageBlocks],
priority: 11,
description: "newRole",
visible: true,
icon: "https://example.com/icon.png",
}),
});
expect(response.status).toBe(403);
const output = await response.json();
expect(output).toMatchObject({
expect(ok).toBe(false);
expect(raw.status).toBe(403);
expect(data).toMatchObject({
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 () => {
const response = await fakeRequest("/api/v1/roles", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "newRole",
await using client = await generateClient(users[0]);
const { data, ok, raw } = await client.createRole("newRole", {
permissions: [RolePermissions.Impersonate],
priority: 1,
description: "newRole",
visible: true,
icon: "https://example.com/icon.png",
}),
});
expect(response.status).toBe(403);
const output = await response.json();
expect(output).toMatchObject({
expect(ok).toBe(false);
expect(raw.status).toBe(403);
expect(data).toMatchObject({
error: "Cannot create role with permissions you do not have",
details: "Forbidden permissions: impersonate",
});

View file

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

View file

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

View file

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

View file

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

View file

@ -1,12 +1,13 @@
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 { Emojis } from "@versia/kit/tables";
import { eq } from "drizzle-orm";
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;
afterAll(async () => {
@ -32,266 +33,172 @@ beforeAll(async () => {
describe("/api/v1/statuses", () => {
test("should return 401 if not authenticated", async () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
body: new URLSearchParams({
status: "Hello, world!",
}),
});
await using client = await generateClient();
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 () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams(),
});
await using client = await generateClient(users[0]);
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 () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "a".repeat(config.validation.notes.max_characters + 1),
local_only: "true",
}),
});
await using client = await generateClient(users[0]);
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 () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "Hello, world!",
visibility: "invalid",
local_only: "true",
}),
await using client = await generateClient(users[0]);
const { ok, raw } = await client.postStatus("Hello, world!", {
visibility: "invalid" as z.infer<typeof Status>["visibility"],
});
expect(response.status).toBe(422);
expect(ok).toBe(false);
expect(raw.status).toBe(422);
});
test("should return 422 if scheduled_at is invalid", async () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "Hello, world!",
scheduled_at: "invalid",
local_only: "true",
}),
await using client = await generateClient(users[0]);
const { data, ok, raw } = await client.postStatus("Hello, world!", {
scheduled_at: new Date(Date.now() - 1000),
});
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 () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "Hello, world!",
await using client = await generateClient(users[0]);
const { ok, raw } = await client.postStatus("Hello, world!", {
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 () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "Hello, world!",
await using client = await generateClient(users[0]);
const { ok, raw } = await client.postStatus("Hello, world!", {
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 () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "Hello, world!",
"media_ids[]": "invalid",
local_only: "true",
}),
await using client = await generateClient(users[0]);
const { ok, raw } = await client.postStatus("Hello, world!", {
media_ids: ["invalid"],
});
expect(response.status).toBe(422);
expect(ok).toBe(false);
expect(raw.status).toBe(422);
});
test("should create a post", async () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "Hello, world!",
local_only: "true",
}),
await using client = await generateClient(users[0]);
const { data, ok } = await client.postStatus("Hello, world!");
expect(ok).toBe(true);
expect(data).toMatchObject({
content: "<p>Hello, world!</p>",
});
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 () => {
// This one uses JSON to test the interop
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
status: "Hello, world!",
await using client = await generateClient(users[0]);
const { data, ok } = await client.postStatus("Hello, world!", {
visibility: "unlisted",
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>");
expect(object.visibility).toBe("unlisted");
expect(ok).toBe(true);
expect(data).toMatchObject({
visibility: "unlisted",
content: "<p>Hello, world!</p>",
});
});
test("should create a post with a reply", async () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
await using client = await generateClient(users[0]);
const { data, ok } = await client.postStatus("Hello, world!");
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(object2.content).toBe("<p>Hello, world again!</p>");
expect(object2.in_reply_to_id).toBe(object.id);
expect(ok2).toBe(true);
expect(data2).toMatchObject({
content: "<p>Hello, world again!</p>",
in_reply_to_id: data.id,
});
});
test("should create a post with a quote", async () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
await using client = await generateClient(users[0]);
const { data, ok } = await client.postStatus("Hello, world!");
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(object2.content).toBe("<p>Hello, world again!</p>");
expect(object2.quote?.id).toBe(object.id);
expect(ok2).toBe(true);
expect(data2).toMatchObject({
content: "<p>Hello, world again!</p>",
quote: expect.objectContaining({
id: data.id,
}),
});
});
test("should correctly parse emojis", async () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "Hello, :test:!",
local_only: "true",
}),
});
await using client = await generateClient(users[0]);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
);
const { data, ok } = await client.postStatus("Hello, :test:!");
const object = (await response.json()) as ApiStatus;
expect(ok).toBe(true);
expect(object.emojis).toBeArrayOfSize(1);
expect(object.emojis[0]).toMatchObject({
expect((data as z.infer<typeof Status>).emojis).toBeArrayOfSize(1);
expect((data as z.infer<typeof Status>).emojis[0]).toMatchObject({
shortcode: "test",
url: expect.stringContaining("/media/proxy/"),
});
@ -299,29 +206,20 @@ describe("/api/v1/statuses", () => {
describe("mentions testing", () => {
test("should correctly parse @mentions", async () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: `Hello, @${users[1].data.username}!`,
local_only: "true",
}),
await using client = await generateClient(users[0]);
const { data, ok } = await client.postStatus(
`Hello, @${users[1].data.username}!`,
);
expect(ok).toBe(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(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
expect((data as z.infer<typeof Status>).mentions).toBeArrayOfSize(
1,
);
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({
expect((data as z.infer<typeof Status>).mentions[0]).toMatchObject({
id: users[1].id,
username: users[1].data.username,
acct: users[1].data.username,
@ -329,28 +227,22 @@ describe("/api/v1/statuses", () => {
});
test("should correctly parse @mentions@domain", async () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: `Hello, @${users[1].data.username}@${
await using client = await generateClient(users[0]);
const { data, ok } = await client.postStatus(
`Hello, @${users[1].data.username}@${
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(object.mentions).toBeArrayOfSize(1);
expect(object.mentions[0]).toMatchObject({
expect(ok).toBe(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(
1,
);
expect((data as z.infer<typeof Status>).mentions[0]).toMatchObject({
id: users[1].id,
username: users[1].data.username,
acct: users[1].data.username,
@ -360,83 +252,51 @@ describe("/api/v1/statuses", () => {
describe("HTML injection testing", () => {
test("should not allow HTML injection", async () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "Hi! <script>alert('Hello, world!');</script>",
local_only: "true",
}),
});
await using client = await generateClient(users[0]);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
const { data, ok } = await client.postStatus(
"Hi! <script>alert('Hello, world!');</script>",
);
const object = (await response.json()) as ApiStatus;
expect(object.content).toBe(
expect(ok).toBe(true);
expect(data).toMatchObject({
content:
"<p>Hi! &lt;script&gt;alert('Hello, world!');&lt;/script&gt;</p>",
);
});
});
test("should not allow HTML injection in spoiler_text", async () => {
const response = await fakeRequest("/api/v1/statuses", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "Hello, world!",
spoiler_text:
"uwu <script>alert('Hello, world!');</script>",
local_only: "true",
}),
await using client = await generateClient(users[0]);
const { data, ok } = await client.postStatus("Hello, world!", {
spoiler_text: "uwu <script>alert('Hello, world!');</script>",
});
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
);
const object = (await response.json()) as ApiStatus;
expect(object.spoiler_text).toBe(
expect(ok).toBe(true);
expect(data).toMatchObject({
spoiler_text:
"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 () => {
const response = await fakeRequest("/api/v1/statuses", {
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",
}),
});
await using client = await generateClient(users[0]);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
const { data, ok } = await client.postStatus(
"<img src='https://example.com/image.jpg'> <video src='https://example.com/video.mp4'> Test!",
);
const object = (await response.json()) as ApiStatus;
expect(ok).toBe(true);
// Proxy url is base_url/media/proxy/<base64url encoded url>
expect(object.content).toBe(
`<p><img src="${config.http.base_url}media/proxy/${Buffer.from(
expect(data).toMatchObject({
content: `<p><img src="${config.http.base_url}media/proxy/${Buffer.from(
"https://example.com/image.jpg",
).toString("base64url")}"> <video src="${
config.http.base_url
}media/proxy/${Buffer.from(
"https://example.com/video.mp4",
).toString("base64url")}"> Test!</p>`,
);
});
});
});
});

View file

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

View file

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

View file

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

View file

@ -1,16 +1,16 @@
import { describe, expect, test } from "bun:test";
import { fakeRequest } from "~/tests/utils";
import { generateClient } from "~/tests/utils";
// /api/v2/instance
describe("/api/v2/instance", () => {
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(json).toBeObject();
expect(json).toContainKeys([
expect(ok).toBe(true);
expect(data).toBeObject();
expect(data).toContainKeys([
"domain",
"title",
"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 { ExtendedDescription } from "../schemas/extended-description.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 { Marker } from "../schemas/marker.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 { Relationship } from "../schemas/relationship.ts";
import type { Report } from "../schemas/report.ts";
import type { Rule } from "../schemas/rule.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 { Token } from "../schemas/token.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);
}
/**
* 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
/**
* POST /api/v1/lists
@ -350,6 +391,18 @@ export class Client extends BaseClient {
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
*
@ -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
*
@ -1122,8 +1197,29 @@ export class Client extends BaseClient {
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
@ -1207,7 +1303,7 @@ export class Client extends BaseClient {
}
/**
* GET /api/v1/instance
* GET /api/v2/instance
*
* @return Instance.
*/
@ -1492,7 +1588,7 @@ export class Client extends BaseClient {
const params = new URLSearchParams();
for (const timeline of timelines) {
params.append("timelines[]", timeline);
params.append("timeline[]", timeline);
}
return this.get<z.infer<typeof Marker> | Record<never, never>>(
@ -1659,6 +1755,8 @@ export class Client extends BaseClient {
since_id: string;
limit: number;
only_media: boolean;
local: boolean;
remote: boolean;
}>,
extra?: RequestInit,
): Promise<Output<z.infer<typeof Status>[]>> {
@ -1680,6 +1778,12 @@ export class Client extends BaseClient {
if (options.only_media) {
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>[]>(
@ -1780,7 +1884,20 @@ export class Client extends BaseClient {
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
*
@ -1790,11 +1907,13 @@ export class Client extends BaseClient {
public getScheduledStatus(
id: string,
extra?: RequestInit,
): Promise<Output<unknown>> {
return this.get<unknown>(`/api/v1/scheduled_statuses/${id}`, extra);
): Promise<Output<z.infer<typeof ScheduledStatus>>> {
return this.get<z.infer<typeof ScheduledStatus>>(
`/api/v1/scheduled_statuses/${id}`,
extra,
);
}
// FIXME: No ScheduledStatus schema
/**
* GET /api/v1/scheduled_statuses
*
@ -1812,7 +1931,7 @@ export class Client extends BaseClient {
limit: number;
}>,
extra?: RequestInit,
): Promise<Output<unknown[]>> {
): Promise<Output<z.infer<typeof ScheduledStatus>[]>> {
const params = new URLSearchParams();
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}`,
extra,
);
@ -2179,7 +2298,6 @@ export class Client extends BaseClient {
);
}
// FIXME: No ScheduledStatus schema
/**
* POST /api/v1/statuses
*
@ -2199,7 +2317,7 @@ export class Client extends BaseClient {
*/
public postStatus(
status: string,
options: {
options?: {
in_reply_to_id?: string;
quote_id?: string;
media_ids?: string[];
@ -2207,7 +2325,7 @@ export class Client extends BaseClient {
spoiler_text?: string;
visibility?: z.infer<typeof Status.shape.visibility>;
content_type?: StatusContentType;
scheduled_at?: string;
scheduled_at?: Date;
language?: string;
local_only?: boolean;
poll?: {
@ -2218,10 +2336,18 @@ export class Client extends BaseClient {
};
},
extra?: RequestInit,
): Promise<Output<z.infer<typeof Status> | unknown>> {
return this.post<z.infer<typeof Status> | unknown>(
): Promise<
Output<z.infer<typeof Status> | z.infer<typeof ScheduledStatus>>
> {
return this.post<
z.infer<typeof Status> | z.infer<typeof ScheduledStatus>
>(
"/api/v1/statuses",
{ status, ...options },
{
status,
...options,
scheduled_at: options?.scheduled_at?.toISOString(),
},
extra,
);
}
@ -2455,15 +2581,31 @@ export class Client extends BaseClient {
};
}>,
extra?: RequestInit,
): Promise<Output<z.infer<typeof Marker>>> {
return this.post<z.infer<typeof Marker>>(
"/api/v1/markers",
options,
extra,
): Promise<
Output<{
home?: z.infer<typeof Marker>;
notifications?: z.infer<typeof Marker>;
}>
> {
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
*
@ -2475,8 +2617,8 @@ export class Client extends BaseClient {
id: string,
scheduled_at?: Date,
extra?: RequestInit,
): Promise<Output<unknown>> {
return this.put<unknown>(
): Promise<Output<z.infer<typeof ScheduledStatus>>> {
return this.put<z.infer<typeof ScheduledStatus>>(
`/api/v1/scheduled_statuses/${id}`,
{ scheduled_at: scheduled_at?.toISOString() },
extra,
@ -2648,7 +2790,13 @@ export class Client extends BaseClient {
): Promise<Output<z.infer<typeof WebPushSubscription>>> {
return this.post<z.infer<typeof WebPushSubscription>>(
"/api/v1/push/subscription",
{ subscription, data },
{
subscription,
policy: data?.policy,
data: {
alerts: data?.alerts,
},
},
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
/**
* PUT /api/v1/lists/:id
@ -2988,7 +3169,7 @@ export class Client extends BaseClient {
): Promise<Output<z.infer<typeof WebPushSubscription>>> {
return this.put<z.infer<typeof WebPushSubscription>>(
"/api/v1/push/subscription",
{ ...data, policy },
{ data, policy },
extra,
);
}

View file

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