Clean up more ActivityPub code, refactoring

This commit is contained in:
Jesse Wierzbinski 2023-10-22 19:39:42 -10:00
parent d05b077df1
commit 80a3e4c92d
No known key found for this signature in database
GPG key ID: F9A1E418934E40B0
26 changed files with 317 additions and 170 deletions

View file

@ -6,13 +6,14 @@ import {
PrimaryGeneratedColumn,
} from "typeorm";
import { APImage, APObject, DateTime } from "activitypub-types";
import { getConfig } from "@config";
import { ConfigType, getConfig } from "@config";
import { appendFile } from "fs/promises";
import { APIStatus } from "~types/entities/status";
import { RawActor } from "./RawActor";
import { APIAccount } from "~types/entities/account";
import { APIEmoji } from "~types/entities/emoji";
import { User } from "./User";
import { Status } from "./Status";
/**
* Represents a raw ActivityPub object in the database.
@ -171,4 +172,57 @@ export class RawObject extends BaseEntity {
static async exists(id: string) {
return !!(await RawObject.getById(id));
}
/**
* Creates a RawObject instance from a Status object.
* DOES NOT SAVE THE OBJECT TO THE DATABASE.
* @param status The Status object to create the RawObject from.
* @returns A Promise that resolves to the RawObject instance.
*/
static createFromStatus(status: Status, config: ConfigType) {
const object = new RawObject();
object.data = {
id: `${config.http.base_url}/users/${status.account.username}/statuses/${status.id}`,
type: "Note",
summary: status.spoiler_text,
content: status.content,
inReplyTo: status.in_reply_to_post?.object.data.id,
published: new Date().toISOString(),
tag: [],
attributedTo: `${config.http.base_url}/users/${status.account.username}`,
};
// Map status mentions to ActivityPub Actor IDs
const mentionedUsers = status.mentions.map(
user => user.actor.data.id as string
);
object.data.to = mentionedUsers;
if (status.visibility === "private") {
object.data.cc = [
`${config.http.base_url}/users/${status.account.username}/followers`,
];
} else if (status.visibility === "direct") {
// Add nothing else
} else if (status.visibility === "public") {
object.data.to = [
...object.data.to,
"https://www.w3.org/ns/activitystreams#Public",
];
object.data.cc = [
`${config.http.base_url}/users/${status.account.username}/followers`,
];
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
else if (status.visibility === "unlisted") {
object.data.to = [
...object.data.to,
"https://www.w3.org/ns/activitystreams#Public",
];
}
return object;
}
}

View file

@ -12,7 +12,7 @@ import {
UpdateDateColumn,
} from "typeorm";
import { APIStatus } from "~types/entities/status";
import { User } from "./User";
import { User, userRelations } from "./User";
import { Application } from "./Application";
import { Emoji } from "./Emoji";
import { RawActivity } from "./RawActivity";
@ -27,7 +27,6 @@ export const statusRelations = [
"object",
"in_reply_to_post",
"instance",
"in_reply_to_account",
"in_reply_to_post.account",
"application",
"emojis",
@ -36,6 +35,16 @@ export const statusRelations = [
"announces",
];
export const statusAndUserRelations = [
...statusRelations,
...[
"account.actor",
"account.relationships",
"account.pinned_notes",
"account.instance",
],
];
/**
* Represents a status (i.e. a post)
*/
@ -122,14 +131,6 @@ export class Status extends BaseEntity {
})
instance!: Instance | null;
/**
* The raw actor that this status is a reply to, if any.
*/
@ManyToOne(() => User, {
nullable: true,
})
in_reply_to_account!: User | null;
/**
* Whether this status is sensitive.
*/
@ -343,26 +344,9 @@ export class Status extends BaseEntity {
newStatus.mentions = [];
newStatus.instance = data.account.instance;
newStatus.object = new RawObject();
if (data.reply) {
newStatus.in_reply_to_post = data.reply.status;
newStatus.in_reply_to_account = data.reply.user;
}
newStatus.object.data = {
id: `${config.http.base_url}/users/${data.account.username}/statuses/${newStatus.id}`,
type: "Note",
summary: data.spoiler_text,
content: data.content,
inReplyTo: data.reply?.status
? data.reply.status.object.data.id
: undefined,
published: new Date().toISOString(),
tag: [],
attributedTo: `${config.http.base_url}/users/${data.account.username}`,
};
// Get people mentioned in the content
const mentionedPeople = [
...data.content.matchAll(/@([a-zA-Z0-9_]+)/g),
@ -370,79 +354,45 @@ export class Status extends BaseEntity {
return `${config.http.base_url}/users/${match[1]}`;
});
// Map this to Users
const mentionedUsers = (
await Promise.all(
mentionedPeople.map(async person => {
// Check if post is in format @username or @username@instance.com
// If is @username, the user is a local user
const instanceUrl =
person.split("@").length === 3
? person.split("@")[2]
: null;
// Get list of mentioned users
await Promise.all(
mentionedPeople.map(async person => {
// Check if post is in format @username or @username@instance.com
// If is @username, the user is a local user
const instanceUrl =
person.split("@").length === 3
? person.split("@")[2]
: null;
if (instanceUrl) {
const user = await User.findOne({
where: {
username: person.split("@")[1],
// If contains instanceUrl
instance: {
base_url: instanceUrl,
},
if (instanceUrl) {
const user = await User.findOne({
where: {
username: person.split("@")[1],
// If contains instanceUrl
instance: {
base_url: instanceUrl,
},
relations: {
actor: true,
instance: true,
},
});
},
relations: userRelations,
});
newStatus.mentions.push(user as User);
newStatus.mentions.push(user as User);
} else {
const user = await User.findOne({
where: {
username: person.split("@")[1],
},
relations: userRelations,
});
return user?.actor.data.id;
} else {
const user = await User.findOne({
where: {
username: person.split("@")[1],
},
relations: {
actor: true,
},
});
newStatus.mentions.push(user as User);
}
})
);
newStatus.mentions.push(user as User);
const object = RawObject.createFromStatus(newStatus, config);
return user?.actor.data.id;
}
})
)
).map(user => user as string);
newStatus.object.data.to = mentionedUsers;
if (data.visibility === "private") {
newStatus.object.data.cc = [
`${config.http.base_url}/users/${data.account.username}/followers`,
];
} else if (data.visibility === "direct") {
// Add nothing else
} else if (data.visibility === "public") {
newStatus.object.data.to = [
...newStatus.object.data.to,
"https://www.w3.org/ns/activitystreams#Public",
];
newStatus.object.data.cc = [
`${config.http.base_url}/users/${data.account.username}/followers`,
];
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
else if (data.visibility === "unlisted") {
newStatus.object.data.to = [
...newStatus.object.data.to,
"https://www.w3.org/ns/activitystreams#Public",
];
}
// TODO: Add default language
newStatus.object = object;
await newStatus.object.save();
await newStatus.save();
return newStatus;
@ -453,11 +403,57 @@ export class Status extends BaseEntity {
* @returns A promise that resolves with the API status.
*/
async toAPI(): Promise<APIStatus> {
const reblogCount = await Status.count({
where: {
reblog: {
id: this.id,
},
},
relations: ["reblog"],
});
const repliesCount = await Status.count({
where: {
in_reply_to_post: {
id: this.id,
},
},
relations: ["in_reply_to_post"],
});
return {
...(await this.object.toAPI()),
id: this.id,
in_reply_to_id: this.in_reply_to_post?.id || null,
in_reply_to_account_id: this.in_reply_to_post?.account.id || null,
account: await this.account.toAPI(),
created_at: new Date(this.created_at).toISOString(),
application: (await this.application?.toAPI()) || null,
card: null,
content: this.content,
emojis: await Promise.all(this.emojis.map(emoji => emoji.toAPI())),
favourited: false,
favourites_count: 0,
media_attachments: [],
mentions: await Promise.all(
this.mentions.map(async m => await m.toAPI())
),
language: null,
muted: false,
pinned: this.account.pinned_notes.some(note => note.id === this.id),
poll: null,
reblog: this.reblog ? await this.reblog.toAPI() : null,
reblogged: !!this.reblog,
reblogs_count: reblogCount,
replies_count: repliesCount,
sensitive: false,
spoiler_text: "",
tags: [],
uri: `${config.http.base_url}/users/${this.account.username}/statuses/${this.id}`,
visibility: "public",
url: `${config.http.base_url}/users/${this.account.username}/statuses/${this.id}`,
bookmarked: false,
quote: null,
quote_id: undefined,
};
}
}

View file

@ -19,13 +19,19 @@ import {
APCollectionPage,
APOrderedCollectionPage,
} from "activitypub-types";
import { RawObject } from "./RawObject";
import { Token } from "./Token";
import { Status, statusRelations } from "./Status";
import { APISource } from "~types/entities/source";
import { Relationship } from "./Relationship";
import { Instance } from "./Instance";
export const userRelations = [
"actor",
"relationships",
"pinned_notes",
"instance",
];
/**
* Represents a user in the database.
* Stores local and remote users
@ -156,9 +162,9 @@ export class User extends BaseEntity {
/**
* The pinned notes for the user.
*/
@ManyToMany(() => RawObject, object => object.id)
@ManyToMany(() => Status, status => status.id)
@JoinTable()
pinned_notes!: RawObject[];
pinned_notes!: Status[];
/**
* Get the user's avatar in raw URL format
@ -296,6 +302,8 @@ export class User extends BaseEntity {
fields: [],
};
user.pinned_notes = [];
await user.generateKeys();
await user.save();
await user.updateActor();
@ -315,12 +323,7 @@ export class User extends BaseEntity {
where: {
access_token,
},
relations: {
user: {
relationships: true,
actor: true,
},
},
relations: userRelations.map(r => `user.${r}`),
});
if (!token) return null;
@ -524,11 +527,48 @@ export class User extends BaseEntity {
relations: ["owner"],
});
const statusCount = await Status.count({
where: {
account: {
id: this.id,
},
},
relations: ["account"],
});
const config = getConfig();
return {
...(await this.actor.toAPIAccount(isOwnAccount)),
id: this.id,
username: this.username,
display_name: this.display_name,
note: this.note,
url: `${config.http.base_url}/users/${this.username}`,
avatar: this.getAvatarUrl(config) || config.defaults.avatar,
header: this.getHeaderUrl(config) || config.defaults.header,
locked: false,
created_at: new Date(this.created_at).toISOString(),
followers_count: follower_count,
following_count: following_count,
statuses_count: statusCount,
emojis: [],
fields: [],
bot: false,
source: isOwnAccount ? this.source : undefined,
avatar_static: "",
header_static: "",
acct:
this.instance === null
? `${this.username}`
: `${this.username}@${this.instance.base_url}`,
limited: false,
moved: null,
noindex: false,
suspended: false,
discoverable: undefined,
mute_expires_at: undefined,
group: false,
role: undefined,
};
}
}

View file

@ -1,6 +1,6 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
import { getConfig, getHost } from "@config";
import { applyConfig } from "@api";
@ -34,7 +34,10 @@ export default async (
return errorResponse("User is a remote user", 404);
}
const user = await User.findOneBy({ username: requestedUser.split("@")[0] });
const user = await User.findOne({
where: { username: requestedUser.split("@")[0] },
relations: userRelations
});
if (!user) {
return errorResponse("User not found", 404);

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
@ -29,8 +29,11 @@ export default async (
if (!self) return errorResponse("Unauthorized", 401);
const user = await User.findOneBy({
id,
const user = await User.findOne({
where: {
id,
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);

View file

@ -2,7 +2,7 @@ import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
@ -36,8 +36,11 @@ export default async (
languages?: string[];
}>(req);
const user = await User.findOneBy({
id,
const user = await User.findOne({
where: {
id,
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);

View file

@ -1,6 +1,6 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
@ -28,8 +28,11 @@ export default async (
let foundUser: User | null;
try {
foundUser = await User.findOneBy({
id,
foundUser = await User.findOne({
where: {
id,
},
relations: userRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);

View file

@ -2,7 +2,7 @@ import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
@ -36,8 +36,11 @@ export default async (
duration: number;
}>(req);
const user = await User.findOneBy({
id,
const user = await User.findOne({
where: {
id,
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);

View file

@ -2,7 +2,7 @@ import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
@ -34,8 +34,11 @@ export default async (
comment: string;
}>(req);
const user = await User.findOneBy({
id,
const user = await User.findOne({
where: {
id,
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
@ -29,8 +29,11 @@ export default async (
if (!self) return errorResponse("Unauthorized", 401);
const user = await User.findOneBy({
id,
const user = await User.findOne({
where: {
id,
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
@ -29,8 +29,11 @@ export default async (
if (!self) return errorResponse("Unauthorized", 401);
const user = await User.findOneBy({
id,
const user = await User.findOne({
where: {
id,
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Status, statusRelations } from "~database/entities/Status";
import { User } from "~database/entities/User";
import { Status, statusAndUserRelations } from "~database/entities/Status";
import { User, userRelations } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
@ -42,8 +42,11 @@ export default async (
tagged?: string;
} = matchedRoute.query;
const user = await User.findOneBy({
id,
const user = await User.findOne({
where: {
id,
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);
@ -60,7 +63,7 @@ export default async (
},
isReblog: exclude_reblogs ? true : undefined,
},
relations: statusRelations,
relations: statusAndUserRelations,
order: {
created_at: "DESC",
},

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
@ -29,8 +29,11 @@ export default async (
if (!self) return errorResponse("Unauthorized", 401);
const user = await User.findOneBy({
id,
const user = await User.findOne({
where: {
id,
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
@ -29,8 +29,11 @@ export default async (
if (!self) return errorResponse("Unauthorized", 401);
const user = await User.findOneBy({
id,
const user = await User.findOne({
where: {
id,
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
@ -29,8 +29,11 @@ export default async (
if (!self) return errorResponse("Unauthorized", 401);
const user = await User.findOneBy({
id,
const user = await User.findOne({
where: {
id,
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
@ -29,8 +29,11 @@ export default async (
if (!self) return errorResponse("Unauthorized", 401);
const user = await User.findOneBy({
id,
const user = await User.findOne({
where: {
id,
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);

View file

@ -1,7 +1,7 @@
import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Status, statusRelations } from "~database/entities/Status";
import { Status, statusAndUserRelations } from "~database/entities/Status";
import { User } from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
@ -35,7 +35,7 @@ export default async (
where: {
id,
},
relations: statusRelations,
relations: statusAndUserRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);

View file

@ -3,7 +3,7 @@ import { applyConfig } from "@api";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { FindManyOptions } from "typeorm";
import { Status, statusRelations } from "~database/entities/Status";
import { Status, statusAndUserRelations } from "~database/entities/Status";
import { User } from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
@ -60,7 +60,7 @@ export default async (req: Request): Promise<Response> => {
created_at: "DESC",
},
take: limit,
relations: statusRelations,
relations: statusAndUserRelations,
};
if (max_id) {

View file

@ -3,7 +3,7 @@ import { applyConfig } from "@api";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { FindManyOptions, IsNull, Not } from "typeorm";
import { Status, statusRelations } from "~database/entities/Status";
import { Status, statusAndUserRelations } from "~database/entities/Status";
import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({
@ -56,7 +56,7 @@ export default async (req: Request): Promise<Response> => {
created_at: "DESC",
},
take: limit,
relations: statusRelations,
relations: statusAndUserRelations,
};
if (max_id) {

View file

@ -4,7 +4,7 @@ import { MatchedRoute } from "bun";
import { randomBytes } from "crypto";
import { Application } from "~database/entities/Application";
import { Token } from "~database/entities/Token";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({
@ -45,8 +45,11 @@ export default async (
return errorResponse("Missing username or password", 400);
// Get user
const user = await User.findOneBy({
email,
const user = await User.findOne({
where: {
email,
},
relations: userRelations,
});
if (!user || !(await Bun.password.verify(password, user.password || "")))
@ -70,5 +73,5 @@ export default async (
await token.save();
// Redirect back to application
return Response.redirect(`${redirect_uri}?code=${token.code}`);
return Response.redirect(`${redirect_uri}?code=${token.code}`, 302);
};

View file

@ -1,6 +1,6 @@
import { errorResponse, jsonLdResponse } from "@response";
import { MatchedRoute } from "bun";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
import { getConfig, getHost } from "@config";
import { applyConfig } from "@api";
@ -34,7 +34,10 @@ export default async (
const username = matchedRoute.params.username;
const user = await User.findOneBy({ username });
const user = await User.findOne({
where: { username },
relations: userRelations,
});
if (!user) {
return errorResponse("User not found", 404);

View file

@ -1,6 +1,6 @@
import { errorResponse, jsonLdResponse } from "@response";
import { MatchedRoute } from "bun";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
import { getHost } from "@config";
import { NodeObject, compact } from "jsonld";
import { RawActivity } from "~database/entities/RawActivity";
@ -30,7 +30,10 @@ export default async (
const min_id = matchedRoute.query.min_id || false;
const max_id = matchedRoute.query.max_id || false;
const user = await User.findOneBy({ username });
const user = await User.findOne({
where: { username },
relations: userRelations,
});
if (!user) {
return errorResponse("User not found", 404);

View file

@ -5,7 +5,7 @@ import { APActor } from "activitypub-types";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { AppDataSource } from "~database/datasource";
import { RawActivity } from "~database/entities/RawActivity";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
const config = getConfig();
@ -65,8 +65,11 @@ describe("POST /@test/actor", () => {
afterAll(async () => {
// Clean up user
const user = await User.findOneBy({
username: "test",
const user = await User.findOne({
where: {
username: "test",
},
relations: userRelations,
});
const activities = await RawActivity.createQueryBuilder("activity")

View file

@ -278,7 +278,7 @@ describe("API Tests", () => {
expect(account.created_at).toBeDefined();
expect(account.followers_count).toBe(0);
expect(account.following_count).toBe(0);
expect(account.statuses_count).toBe(0);
expect(account.statuses_count).toBe(2);
expect(account.note).toBe("");
expect(account.url).toBe(
`${config.http.base_url}/users/${user.username}`

View file

@ -3,7 +3,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { AppDataSource } from "~database/datasource";
import { RawActivity } from "~database/entities/RawActivity";
import { Token } from "~database/entities/Token";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
const config = getConfig();
@ -275,8 +275,11 @@ describe("POST /@test/inbox", () => {
afterAll(async () => {
// Clean up user
const user = await User.findOneBy({
username: "test",
const user = await User.findOne({
where: {
username: "test",
},
relations: userRelations,
});
// Clean up tokens

View file

@ -3,7 +3,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { AppDataSource } from "~database/datasource";
import { Application } from "~database/entities/Application";
import { Token } from "~database/entities/Token";
import { User } from "~database/entities/User";
import { User, userRelations } from "~database/entities/User";
const config = getConfig();
@ -150,8 +150,11 @@ describe("GET /api/v1/apps/verify_credentials", () => {
afterAll(async () => {
// Clean up user
const user = await User.findOneBy({
username: "test",
const user = await User.findOne({
where: {
username: "test",
},
relations: userRelations,
});
// Clean up tokens