Add more tests, fix roiutes

This commit is contained in:
Jesse Wierzbinski 2024-04-11 13:12:23 -10:00
parent 0469187876
commit 3ccff003f5
No known key found for this signature in database
11 changed files with 923 additions and 80 deletions

View file

@ -847,7 +847,7 @@ export const createNewStatus = async (
content["text/markdown"]?.content || content["text/markdown"]?.content ||
"", "",
contentType: "text/html", contentType: "text/html",
visibility: visibility, visibility,
sensitive: is_sensitive, sensitive: is_sensitive,
spoilerText: spoiler_text, spoilerText: spoiler_text,
instanceId: author.instanceId || null, instanceId: author.instanceId || null,
@ -1148,7 +1148,7 @@ export const statusToAPI = async (
`/@${statusToConvert.author.username}/${statusToConvert.id}`, `/@${statusToConvert.author.username}/${statusToConvert.id}`,
config.http.base_url, config.http.base_url,
).toString(), ).toString(),
visibility: "public", visibility: statusToConvert.visibility as APIStatus["visibility"],
url: url:
statusToConvert.uri || statusToConvert.uri ||
new URL( new URL(

View file

@ -0,0 +1,110 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import {
deleteOldTestUsers,
getTestStatuses,
getTestUsers,
sendTestRequest,
} from "~tests/utils";
import { config } from "~index";
import { meta } from "./index";
import type { APIStatus } from "~types/entities/status";
import type { APIAccount } from "~types/entities/account";
import { getUserUri } from "~database/entities/User";
await deleteOldTestUsers();
const { users, tokens, deleteUsers } = await getTestUsers(5);
const timeline = (await getTestStatuses(40, users[0])).toReversed();
afterAll(async () => {
await deleteUsers();
});
beforeAll(async () => {
for (const status of timeline) {
await fetch(
new URL(
`/api/v1/statuses/${status.id}/favourite`,
config.http.base_url,
),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].accessToken}`,
},
},
);
}
});
// /api/v1/accounts/:id
describe(meta.route, () => {
test("should return 401 if not authenticated and trying to use following", async () => {
const response = await sendTestRequest(
new Request(
new URL(
`${meta.route.replace(":id", users[0].id)}?following=true`,
config.http.base_url,
),
),
);
expect(response.status).toBe(401);
});
test("should return 404 if ID is invalid", async () => {
const response = await sendTestRequest(
new Request(
new URL(
meta.route.replace(":id", "invalid"),
config.http.base_url,
),
),
);
expect(response.status).toBe(404);
});
test("should return user", async () => {
const response = await sendTestRequest(
new Request(
new URL(
meta.route.replace(":id", users[0].id),
config.http.base_url,
),
),
);
expect(response.status).toBe(200);
const data = (await response.json()) as APIAccount;
expect(data).toMatchObject({
id: users[0].id,
username: users[0].username,
display_name: users[0].displayName,
avatar: expect.any(String),
header: expect.any(String),
locked: users[0].isLocked,
created_at: new Date(users[0].createdAt).toISOString(),
followers_count: 0,
following_count: 0,
statuses_count: 40,
note: users[0].note,
acct: users[0].username,
url: expect.any(String),
avatar_static: expect.any(String),
header_static: expect.any(String),
emojis: [],
moved: null,
fields: [],
bot: false,
group: false,
limited: false,
noindex: false,
suspended: false,
pleroma: {
is_admin: false,
is_moderator: false,
},
} satisfies APIAccount);
});
});

View file

@ -1,9 +1,6 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { findFirstUser, userToAPI } from "~database/entities/User";
import type { UserWithRelations } from "~database/entities/User";
import { userToAPI } from "~database/entities/User";
import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -34,15 +31,9 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
const { user } = extraData.auth; const { user } = extraData.auth;
let foundUser: UserWithRelations | null; const foundUser = await findFirstUser({
try { where: (user, { eq }) => eq(user.id, id),
foundUser = await client.user.findUnique({ }).catch(() => null);
where: { id },
include: userRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);
}
if (!foundUser) return errorResponse("User not found", 404); if (!foundUser) return errorResponse("User not found", 404);

View file

@ -0,0 +1,91 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import {
deleteOldTestUsers,
getTestStatuses,
getTestUsers,
sendTestRequest,
} from "~tests/utils";
import { config } from "~index";
import { meta } from "./index";
import type { APIStatus } from "~types/entities/status";
import type { APIAccount } from "~types/entities/account";
import { getUserUri } from "~database/entities/User";
await deleteOldTestUsers();
const { users, tokens, deleteUsers } = await getTestUsers(5);
afterAll(async () => {
await deleteUsers();
});
// /api/v1/accounts/lookup
describe(meta.route, () => {
test("should return 400 if acct is missing", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
}),
);
expect(response.status).toBe(400);
});
test("should return 400 if acct is empty", async () => {
const response = await sendTestRequest(
new Request(new URL(`${meta.route}?acct=`, config.http.base_url), {
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
}),
);
expect(response.status).toBe(400);
});
test("should return 404 if acct is invalid", async () => {
const response = await sendTestRequest(
new Request(
new URL(`${meta.route}?acct=invalid`, config.http.base_url),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(404);
});
test("should return 200 with users", async () => {
const response = await sendTestRequest(
new Request(
new URL(
`${meta.route}?acct=${users[0].username}`,
config.http.base_url,
),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(200);
const data = (await response.json()) as APIAccount[];
expect(data).toEqual(
expect.objectContaining({
id: users[0].id,
username: users[0].username,
display_name: users[0].displayName,
avatar: expect.any(String),
header: expect.any(String),
}),
);
});
});

View file

@ -1,9 +1,10 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import {
import type { UserWithRelations } from "~database/entities/User"; findFirstUser,
import { resolveWebFinger, userToAPI } from "~database/entities/User"; resolveWebFinger,
import { userRelations } from "~database/entities/relations"; userToAPI,
} from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -57,11 +58,8 @@ export default apiRoute<{
username = username.slice(1); username = username.slice(1);
} }
const account = await client.user.findFirst({ const account = await findFirstUser({
where: { where: (user, { eq }) => eq(user.username, username),
username,
},
include: userRelations,
}); });
if (account) { if (account) {

View file

@ -0,0 +1,114 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import {
deleteOldTestUsers,
getTestStatuses,
getTestUsers,
sendTestRequest,
} from "~tests/utils";
import { config } from "~index";
import { meta } from "./index";
import type { APIStatus } from "~types/entities/status";
import type { APIAccount } from "~types/entities/account";
import { getUserUri } from "~database/entities/User";
await deleteOldTestUsers();
const { users, tokens, deleteUsers } = await getTestUsers(5);
afterAll(async () => {
await deleteUsers();
});
// /api/v1/accounts/search
describe(meta.route, () => {
test("should return 400 if q is missing", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
}),
);
expect(response.status).toBe(400);
});
test("should return 400 if q is empty", async () => {
const response = await sendTestRequest(
new Request(new URL(`${meta.route}?q=`, config.http.base_url), {
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
}),
);
expect(response.status).toBe(400);
});
test("should return 400 if limit is less than 1", async () => {
const response = await sendTestRequest(
new Request(
new URL(
`${meta.route}?q=${users[0].username}&limit=0`,
config.http.base_url,
),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(400);
});
test("should return 400 if limit is greater than 80", async () => {
const response = await sendTestRequest(
new Request(
new URL(
`${meta.route}?q=${users[0].username}&limit=100`,
config.http.base_url,
),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(400);
});
test("should return 200 with users", async () => {
const response = await sendTestRequest(
new Request(
new URL(
`${meta.route}?q=${users[0].username}`,
config.http.base_url,
),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(200);
const data = (await response.json()) as APIAccount[];
expect(data).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: users[0].id,
username: users[0].username,
display_name: users[0].displayName,
avatar: expect.any(String),
header: expect.any(String),
}),
]),
);
});
});

View file

@ -1,8 +1,13 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { sql } from "drizzle-orm";
import { userToAPI } from "~database/entities/User"; import {
import { userRelations } from "~database/entities/relations"; findManyUsers,
resolveWebFinger,
userToAPI,
type UserWithRelations,
} from "~database/entities/User";
import { user } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -29,46 +34,47 @@ export default apiRoute<{
following = false, following = false,
limit = 40, limit = 40,
offset, offset,
resolve,
q, q,
} = extraData.parsedRequest; } = extraData.parsedRequest;
const { user } = extraData.auth; const { user: self } = extraData.auth;
if (!user && following) return errorResponse("Unauthorized", 401); if (!self && following) return errorResponse("Unauthorized", 401);
if (limit < 1 || limit > 80) { if (limit < 1 || limit > 80) {
return errorResponse("Limit must be between 1 and 80", 400); return errorResponse("Limit must be between 1 and 80", 400);
} }
// TODO: Add WebFinger resolve if (!q) {
return errorResponse("Query is required", 400);
}
const accounts = await client.user.findMany({ const [username, host] = q?.split("@") || [];
where: {
OR: [ const accounts: UserWithRelations[] = [];
{
displayName: { if (resolve && username && host) {
contains: q, const resolvedUser = await resolveWebFinger(username, host);
},
}, if (resolvedUser) {
{ accounts.push(resolvedUser);
username: { }
contains: q, } else {
}, accounts.push(
}, ...(await findManyUsers({
], where: (account, { or, like }) =>
relationshipSubjects: following or(
? { like(account.displayName, `%${q}%`),
some: { like(account.username, `%${q}%`),
ownerId: user?.id, following
following, ? sql`EXISTS (SELECT 1 FROM "Relationship" WHERE "Relationship"."subjectId" = ${user.id} AND "Relationship"."ownerId" = ${account.id} AND "Relationship"."following" = true)`
}, : undefined,
} ),
: undefined, offset: Number(offset),
}, })),
take: Number(limit), );
skip: Number(offset || 0), }
include: userRelations,
});
return jsonResponse(accounts.map((acct) => userToAPI(acct))); return jsonResponse(accounts.map((acct) => userToAPI(acct)));
}); });

View file

@ -0,0 +1,116 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import {
deleteOldTestUsers,
getTestStatuses,
getTestUsers,
sendTestRequest,
} from "~tests/utils";
import { config } from "~index";
import { meta } from "./favourited_by";
import type { APIStatus } from "~types/entities/status";
import type { APIAccount } from "~types/entities/account";
await deleteOldTestUsers();
const { users, tokens, deleteUsers } = await getTestUsers(5);
const timeline = (await getTestStatuses(40, users[0])).toReversed();
afterAll(async () => {
await deleteUsers();
});
beforeAll(async () => {
for (const status of timeline) {
await fetch(
new URL(
`/api/v1/statuses/${status.id}/favourite`,
config.http.base_url,
),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].accessToken}`,
},
},
);
}
});
// /api/v1/statuses/:id/favourited_by
describe(meta.route, () => {
test("should return 401 if not authenticated", async () => {
const response = await sendTestRequest(
new Request(
new URL(
meta.route.replace(":id", timeline[0].id),
config.http.base_url,
),
),
);
expect(response.status).toBe(401);
});
test("should return 400 if limit is less than 1", async () => {
const response = await sendTestRequest(
new Request(
new URL(
`${meta.route.replace(":id", timeline[0].id)}?limit=0`,
config.http.base_url,
),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(400);
});
test("should return 400 if limit is greater than 80", async () => {
const response = await sendTestRequest(
new Request(
new URL(
`${meta.route.replace(":id", timeline[0].id)}?limit=100`,
config.http.base_url,
),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(400);
});
test("should return 200 with users", async () => {
const response = await sendTestRequest(
new Request(
new URL(
meta.route.replace(":id", timeline[0].id),
config.http.base_url,
),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("application/json");
const objects = (await response.json()) as APIAccount[];
expect(objects.length).toBe(1);
for (const [index, status] of objects.entries()) {
expect(status.id).toBe(users[1].id);
expect(status.username).toBe(users[1].username);
}
});
});

View file

@ -0,0 +1,116 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import {
deleteOldTestUsers,
getTestStatuses,
getTestUsers,
sendTestRequest,
} from "~tests/utils";
import { config } from "~index";
import { meta } from "./reblogged_by";
import type { APIStatus } from "~types/entities/status";
import type { APIAccount } from "~types/entities/account";
await deleteOldTestUsers();
const { users, tokens, deleteUsers } = await getTestUsers(5);
const timeline = (await getTestStatuses(40, users[0])).toReversed();
afterAll(async () => {
await deleteUsers();
});
beforeAll(async () => {
for (const status of timeline) {
await fetch(
new URL(
`/api/v1/statuses/${status.id}/reblog`,
config.http.base_url,
),
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens[1].accessToken}`,
},
},
);
}
});
// /api/v1/statuses/:id/reblogged_by
describe(meta.route, () => {
test("should return 401 if not authenticated", async () => {
const response = await sendTestRequest(
new Request(
new URL(
meta.route.replace(":id", timeline[0].id),
config.http.base_url,
),
),
);
expect(response.status).toBe(401);
});
test("should return 400 if limit is less than 1", async () => {
const response = await sendTestRequest(
new Request(
new URL(
`${meta.route.replace(":id", timeline[0].id)}?limit=0`,
config.http.base_url,
),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(400);
});
test("should return 400 if limit is greater than 80", async () => {
const response = await sendTestRequest(
new Request(
new URL(
`${meta.route.replace(":id", timeline[0].id)}?limit=100`,
config.http.base_url,
),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(400);
});
test("should return 200 with users", async () => {
const response = await sendTestRequest(
new Request(
new URL(
meta.route.replace(":id", timeline[0].id),
config.http.base_url,
),
{
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
},
),
);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("application/json");
const objects = (await response.json()) as APIAccount[];
expect(objects.length).toBe(1);
for (const [index, status] of objects.entries()) {
expect(status.id).toBe(users[1].id);
expect(status.username).toBe(users[1].username);
}
});
});

View file

@ -0,0 +1,297 @@
import { afterAll, describe, expect, test } from "bun:test";
import {
deleteOldTestUsers,
getTestUsers,
sendTestRequest,
} from "~tests/utils";
import { config } from "~index";
import { meta } from "./index";
import type { APIStatus } from "~types/entities/status";
await deleteOldTestUsers();
const { users, tokens, deleteUsers } = await getTestUsers(5);
afterAll(async () => {
await deleteUsers();
});
describe(meta.route, () => {
test("should return 405 if method is not allowed", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "GET",
}),
);
expect(response.status).toBe(405);
});
test("should return 401 if not authenticated", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
}),
);
expect(response.status).toBe(401);
});
test("should return 422 is status is empty", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens[0].accessToken}`,
},
body: JSON.stringify({}),
}),
);
expect(response.status).toBe(422);
});
test("should return 400 is status is too long", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens[0].accessToken}`,
},
body: JSON.stringify({
status: "a".repeat(config.validation.max_note_size + 1),
federate: false,
}),
}),
);
expect(response.status).toBe(400);
});
test("should return 422 is visibility is invalid", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens[0].accessToken}`,
},
body: JSON.stringify({
status: "Hello, world!",
visibility: "invalid",
federate: false,
}),
}),
);
expect(response.status).toBe(422);
});
test("should return 422 if scheduled_at is invalid", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens[0].accessToken}`,
},
body: JSON.stringify({
status: "Hello, world!",
scheduled_at: "invalid",
federate: false,
}),
}),
);
expect(response.status).toBe(422);
});
test("should return 404 is in_reply_to_id is invalid", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens[0].accessToken}`,
},
body: JSON.stringify({
status: "Hello, world!",
in_reply_to_id: "invalid",
federate: false,
}),
}),
);
expect(response.status).toBe(404);
});
test("should return 404 is quote_id is invalid", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens[0].accessToken}`,
},
body: JSON.stringify({
status: "Hello, world!",
quote_id: "invalid",
federate: false,
}),
}),
);
expect(response.status).toBe(404);
});
test("should return 422 is media_ids is invalid", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens[0].accessToken}`,
},
body: JSON.stringify({
status: "Hello, world!",
media_ids: ["invalid"],
federate: false,
}),
}),
);
expect(response.status).toBe(422);
});
test("should create a post", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens[0].accessToken}`,
},
body: JSON.stringify({
status: "Hello, world!",
federate: false,
}),
}),
);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("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 () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens[0].accessToken}`,
},
body: JSON.stringify({
status: "Hello, world!",
visibility: "unlisted",
federate: false,
}),
}),
);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("application/json");
const object = (await response.json()) as APIStatus;
expect(object.content).toBe("<p>Hello, world!</p>");
expect(object.visibility).toBe("unlisted");
});
test("should create a post with a reply", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens[0].accessToken}`,
},
body: JSON.stringify({
status: "Hello, world!",
federate: false,
}),
}),
);
const object = (await response.json()) as APIStatus;
const response2 = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens[0].accessToken}`,
},
body: JSON.stringify({
status: "Hello, world again!",
in_reply_to_id: object.id,
federate: false,
}),
}),
);
expect(response2.status).toBe(200);
expect(response2.headers.get("content-type")).toBe("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);
});
test("should create a post with a quote", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens[0].accessToken}`,
},
body: JSON.stringify({
status: "Hello, world!",
federate: false,
}),
}),
);
const object = (await response.json()) as APIStatus;
const response2 = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens[0].accessToken}`,
},
body: JSON.stringify({
status: "Hello, world again!",
quote_id: object.id,
federate: false,
}),
}),
);
expect(response2.status).toBe(200);
expect(response2.headers.get("content-type")).toBe("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);
});
});

View file

@ -14,6 +14,7 @@ import {
} from "~database/entities/Status"; } from "~database/entities/Status";
import type { UserWithRelations } from "~database/entities/User"; import type { UserWithRelations } from "~database/entities/User";
import { statusAndUserRelations } from "~database/entities/relations"; import { statusAndUserRelations } from "~database/entities/relations";
import { db } from "~drizzle/db";
import type { APIStatus } from "~types/entities/status"; import type { APIStatus } from "~types/entities/status";
export const meta = applyConfig({ export const meta = applyConfig({
@ -49,7 +50,7 @@ export default apiRoute<{
content_type?: string; content_type?: string;
federate?: boolean; federate?: boolean;
}>(async (req, matchedRoute, extraData) => { }>(async (req, matchedRoute, extraData) => {
const { user, token } = extraData.auth; const { user } = extraData.auth;
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
@ -138,11 +139,22 @@ export default apiRoute<{
} }
if (scheduled_at) { if (scheduled_at) {
if (new Date(scheduled_at).getTime() < Date.now()) { if (
Number.isNaN(new Date(scheduled_at).getTime()) ||
new Date(scheduled_at).getTime() < Date.now()
) {
return errorResponse("Scheduled time must be in the future", 422); return errorResponse("Scheduled time must be in the future", 422);
} }
} }
// Validate visibility
if (
visibility &&
!["public", "unlisted", "private", "direct"].includes(visibility)
) {
return errorResponse("Invalid visibility", 422);
}
let sanitizedStatus: string; let sanitizedStatus: string;
if (content_type === "text/markdown") { if (content_type === "text/markdown") {
@ -162,14 +174,6 @@ export default apiRoute<{
); );
} }
// Validate visibility
if (
visibility &&
!["public", "unlisted", "private", "direct"].includes(visibility)
) {
return errorResponse("Invalid visibility", 422);
}
// Get reply account and status if exists // Get reply account and status if exists
let replyStatus: StatusWithRelations | null = null; let replyStatus: StatusWithRelations | null = null;
let quote: StatusWithRelations | null = null; let quote: StatusWithRelations | null = null;
@ -177,7 +181,7 @@ export default apiRoute<{
if (in_reply_to_id) { if (in_reply_to_id) {
replyStatus = await findFirstStatuses({ replyStatus = await findFirstStatuses({
where: (status, { eq }) => eq(status.id, in_reply_to_id), where: (status, { eq }) => eq(status.id, in_reply_to_id),
}); }).catch(() => null);
if (!replyStatus) { if (!replyStatus) {
return errorResponse("Reply status not found", 404); return errorResponse("Reply status not found", 404);
@ -187,7 +191,7 @@ export default apiRoute<{
if (quote_id) { if (quote_id) {
quote = await findFirstStatuses({ quote = await findFirstStatuses({
where: (status, { eq }) => eq(status.id, quote_id), where: (status, { eq }) => eq(status.id, quote_id),
}); }).catch(() => null);
if (!quote) { if (!quote) {
return errorResponse("Quote status not found", 404); return errorResponse("Quote status not found", 404);
@ -200,17 +204,17 @@ export default apiRoute<{
} }
// Check if media attachments are all valid // Check if media attachments are all valid
if (media_ids && media_ids.length > 0) {
const foundAttachments = await db.query.attachment
.findMany({
where: (attachment, { inArray }) =>
inArray(attachment.id, media_ids),
})
.catch(() => []);
const foundAttachments = await client.attachment.findMany({ if (foundAttachments.length !== (media_ids ?? []).length) {
where: { return errorResponse("Invalid media IDs", 422);
id: { }
in: media_ids ?? [],
},
},
});
if (foundAttachments.length !== (media_ids ?? []).length) {
return errorResponse("Invalid media IDs", 422);
} }
const mentions = await parseTextMentions(sanitizedStatus); const mentions = await parseTextMentions(sanitizedStatus);
@ -222,7 +226,7 @@ export default apiRoute<{
content: sanitizedStatus ?? "", content: sanitizedStatus ?? "",
}, },
}, },
visibility as APIStatus["visibility"], visibility ?? "public",
sensitive ?? false, sensitive ?? false,
spoiler_text ?? "", spoiler_text ?? "",
[], [],