mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
Add more API endpoints, better tests
This commit is contained in:
parent
dacbc5a00b
commit
6d2f9072ac
|
|
@ -1,5 +1,6 @@
|
||||||
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
import { APIApplication } from "~types/entities/application";
|
import { APIApplication } from "~types/entities/application";
|
||||||
|
import { Token } from "./Token";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applications from clients
|
* Applications from clients
|
||||||
|
|
@ -36,6 +37,16 @@ export class Application extends BaseEntity {
|
||||||
@Column("varchar")
|
@Column("varchar")
|
||||||
redirect_uris = "urn:ietf:wg:oauth:2.0:oob";
|
redirect_uris = "urn:ietf:wg:oauth:2.0:oob";
|
||||||
|
|
||||||
|
static async getFromToken(token: string) {
|
||||||
|
const dbToken = await Token.findOne({
|
||||||
|
where: {
|
||||||
|
access_token: token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return dbToken?.application || null;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
async toAPI(): Promise<APIApplication> {
|
async toAPI(): Promise<APIApplication> {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,6 @@ import { RawActor } from "./RawActor";
|
||||||
import { getConfig } from "@config";
|
import { getConfig } from "@config";
|
||||||
import { errorResponse } from "@response";
|
import { errorResponse } from "@response";
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores an ActivityPub activity as raw JSON-LD data
|
|
||||||
*/
|
|
||||||
@Entity({
|
@Entity({
|
||||||
name: "activities",
|
name: "activities",
|
||||||
})
|
})
|
||||||
|
|
@ -25,25 +22,19 @@ export class RawActivity extends BaseEntity {
|
||||||
@Column("jsonb")
|
@Column("jsonb")
|
||||||
data!: APActivity;
|
data!: APActivity;
|
||||||
|
|
||||||
// Any associated objects (there is typically only one)
|
@ManyToMany(() => RawObject)
|
||||||
@ManyToMany(() => RawObject, object => object.id)
|
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
objects!: RawObject[];
|
objects!: RawObject[];
|
||||||
|
|
||||||
@ManyToMany(() => RawActor, actor => actor.id)
|
@ManyToMany(() => RawActor)
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
actors!: RawActor[];
|
actors!: RawActor[];
|
||||||
|
|
||||||
static async getByObjectId(id: string) {
|
static async getByObjectId(id: string) {
|
||||||
return await RawActivity.createQueryBuilder("activity")
|
return await RawActivity.createQueryBuilder("activity")
|
||||||
// Objects is a many-to-many relationship
|
|
||||||
.leftJoinAndSelect("activity.objects", "objects")
|
.leftJoinAndSelect("activity.objects", "objects")
|
||||||
.leftJoinAndSelect("activity.actors", "actors")
|
.leftJoinAndSelect("activity.actors", "actors")
|
||||||
.where("objects.data @> :data", {
|
.where("objects.data @> :data", { data: JSON.stringify({ id }) })
|
||||||
data: JSON.stringify({
|
|
||||||
id,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,21 +42,15 @@ export class RawActivity extends BaseEntity {
|
||||||
return await RawActivity.createQueryBuilder("activity")
|
return await RawActivity.createQueryBuilder("activity")
|
||||||
.leftJoinAndSelect("activity.objects", "objects")
|
.leftJoinAndSelect("activity.objects", "objects")
|
||||||
.leftJoinAndSelect("activity.actors", "actors")
|
.leftJoinAndSelect("activity.actors", "actors")
|
||||||
.where("activity.data->>'id' = :id", {
|
.where("activity.data->>'id' = :id", { id })
|
||||||
id,
|
|
||||||
})
|
|
||||||
.getOne();
|
.getOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getLatestById(id: string) {
|
static async getLatestById(id: string) {
|
||||||
return await RawActivity.createQueryBuilder("activity")
|
return await RawActivity.createQueryBuilder("activity")
|
||||||
// Where id is part of the jsonb column 'data'
|
.where("activity.data->>'id' = :id", { id })
|
||||||
.where("activity.data->>'id' = :id", {
|
|
||||||
id,
|
|
||||||
})
|
|
||||||
.leftJoinAndSelect("activity.objects", "objects")
|
.leftJoinAndSelect("activity.objects", "objects")
|
||||||
.leftJoinAndSelect("activity.actors", "actors")
|
.leftJoinAndSelect("activity.actors", "actors")
|
||||||
// Sort by most recent
|
|
||||||
.orderBy("activity.data->>'published'", "DESC")
|
.orderBy("activity.data->>'published'", "DESC")
|
||||||
.getOne();
|
.getOne();
|
||||||
}
|
}
|
||||||
|
|
@ -77,24 +62,26 @@ export class RawActivity extends BaseEntity {
|
||||||
static async updateObjectIfExists(object: APObject) {
|
static async updateObjectIfExists(object: APObject) {
|
||||||
const rawObject = await RawObject.getById(object.id ?? "");
|
const rawObject = await RawObject.getById(object.id ?? "");
|
||||||
|
|
||||||
if (rawObject) {
|
if (!rawObject) {
|
||||||
rawObject.data = object;
|
|
||||||
|
|
||||||
// Check if object body contains any filtered terms
|
|
||||||
if (await rawObject.isObjectFiltered())
|
|
||||||
return errorResponse("Object filtered", 409);
|
|
||||||
|
|
||||||
await rawObject.save();
|
|
||||||
return rawObject;
|
|
||||||
} else {
|
|
||||||
return errorResponse("Object does not exist", 404);
|
return errorResponse("Object does not exist", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rawObject.data = object;
|
||||||
|
|
||||||
|
if (await rawObject.isObjectFiltered()) {
|
||||||
|
return errorResponse("Object filtered", 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
await rawObject.save();
|
||||||
|
return rawObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async deleteObjectIfExists(object: APObject) {
|
static async deleteObjectIfExists(object: APObject) {
|
||||||
const dbObject = await RawObject.getById(object.id ?? "");
|
const dbObject = await RawObject.getById(object.id ?? "");
|
||||||
|
|
||||||
if (!dbObject) return errorResponse("Object does not exist", 404);
|
if (!dbObject) {
|
||||||
|
return errorResponse("Object does not exist", 404);
|
||||||
|
}
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
|
|
@ -110,16 +97,12 @@ export class RawActivity extends BaseEntity {
|
||||||
} else {
|
} else {
|
||||||
const activities = await RawActivity.getByObjectId(object.id ?? "");
|
const activities = await RawActivity.getByObjectId(object.id ?? "");
|
||||||
|
|
||||||
activities.forEach(
|
for (const activity of activities) {
|
||||||
activity =>
|
activity.objects = activity.objects.filter(
|
||||||
(activity.objects = activity.objects.filter(
|
o => o.id !== object.id
|
||||||
o => o.id !== object.id
|
);
|
||||||
))
|
await activity.save();
|
||||||
);
|
}
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
activities.map(async activity => await activity.save())
|
|
||||||
);
|
|
||||||
|
|
||||||
await dbObject.remove();
|
await dbObject.remove();
|
||||||
}
|
}
|
||||||
|
|
@ -127,23 +110,27 @@ export class RawActivity extends BaseEntity {
|
||||||
return dbObject;
|
return dbObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async addIfNotExists(activity: APActivity) {
|
static async addIfNotExists(activity: APActivity, addObject?: RawObject) {
|
||||||
if (!(await RawActivity.exists(activity.id ?? ""))) {
|
if (await RawActivity.exists(activity.id ?? "")) {
|
||||||
const rawActivity = new RawActivity();
|
return errorResponse("Activity already exists", 409);
|
||||||
rawActivity.data = {
|
}
|
||||||
...activity,
|
|
||||||
object: undefined,
|
|
||||||
actor: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const actor = await rawActivity.addActorIfNotExists(
|
const rawActivity = new RawActivity();
|
||||||
activity.actor as APActor
|
rawActivity.data = { ...activity, object: undefined, actor: undefined };
|
||||||
);
|
rawActivity.actors = [];
|
||||||
|
rawActivity.objects = [];
|
||||||
|
|
||||||
if (actor instanceof Response) {
|
const actor = await rawActivity.addActorIfNotExists(
|
||||||
return actor;
|
activity.actor as APActor
|
||||||
}
|
);
|
||||||
|
|
||||||
|
if (actor instanceof Response) {
|
||||||
|
return actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addObject) {
|
||||||
|
rawActivity.objects.push(addObject);
|
||||||
|
} else {
|
||||||
const object = await rawActivity.addObjectIfNotExists(
|
const object = await rawActivity.addObjectIfNotExists(
|
||||||
activity.object as APObject
|
activity.object as APObject
|
||||||
);
|
);
|
||||||
|
|
@ -151,67 +138,77 @@ export class RawActivity extends BaseEntity {
|
||||||
if (object instanceof Response) {
|
if (object instanceof Response) {
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
await rawActivity.save();
|
|
||||||
return rawActivity;
|
|
||||||
} else {
|
|
||||||
return errorResponse("Activity already exists", 409);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await rawActivity.save();
|
||||||
|
return rawActivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeActivityPubRepresentation() {
|
||||||
|
return {
|
||||||
|
...this.data,
|
||||||
|
object: this.objects[0].data,
|
||||||
|
actor: this.actors[0].data,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async addObjectIfNotExists(object: APObject) {
|
async addObjectIfNotExists(object: APObject) {
|
||||||
if (!this.objects.some(o => o.data.id === object.id)) {
|
if (this.objects.some(o => o.data.id === object.id)) {
|
||||||
const rawObject = new RawObject();
|
|
||||||
rawObject.data = object;
|
|
||||||
|
|
||||||
// Check if object body contains any filtered terms
|
|
||||||
if (await rawObject.isObjectFiltered())
|
|
||||||
return errorResponse("Object filtered", 409);
|
|
||||||
|
|
||||||
await rawObject.save();
|
|
||||||
|
|
||||||
this.objects.push(rawObject);
|
|
||||||
|
|
||||||
return rawObject;
|
|
||||||
} else {
|
|
||||||
return errorResponse("Object already exists", 409);
|
return errorResponse("Object already exists", 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rawObject = new RawObject();
|
||||||
|
rawObject.data = object;
|
||||||
|
|
||||||
|
if (await rawObject.isObjectFiltered()) {
|
||||||
|
return errorResponse("Object filtered", 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
await rawObject.save();
|
||||||
|
this.objects.push(rawObject);
|
||||||
|
return rawObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addActorIfNotExists(actor: APActor) {
|
async addActorIfNotExists(actor: APActor) {
|
||||||
if (!this.actors.some(a => a.data.id === actor.id)) {
|
const dbActor = await RawActor.getByActorId(actor.id ?? "");
|
||||||
const rawActor = new RawActor();
|
|
||||||
rawActor.data = actor;
|
|
||||||
|
|
||||||
const config = getConfig();
|
if (dbActor) {
|
||||||
|
this.actors.push(dbActor);
|
||||||
|
return dbActor;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (this.actors.some(a => a.data.id === actor.id)) {
|
||||||
config.activitypub.discard_avatars.find(
|
|
||||||
instance => actor.id?.includes(instance)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
rawActor.data.icon = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
config.activitypub.discard_banners.find(
|
|
||||||
instance => actor.id?.includes(instance)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
rawActor.data.image = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await rawActor.isObjectFiltered()) {
|
|
||||||
return errorResponse("Actor filtered", 409);
|
|
||||||
}
|
|
||||||
|
|
||||||
await rawActor.save();
|
|
||||||
|
|
||||||
this.actors.push(rawActor);
|
|
||||||
|
|
||||||
return rawActor;
|
|
||||||
} else {
|
|
||||||
return errorResponse("Actor already exists", 409);
|
return errorResponse("Actor already exists", 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rawActor = new RawActor();
|
||||||
|
rawActor.data = actor;
|
||||||
|
rawActor.followers = [];
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
if (
|
||||||
|
config.activitypub.discard_avatars.find(
|
||||||
|
instance => actor.id?.includes(instance)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
rawActor.data.icon = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
config.activitypub.discard_banners.find(
|
||||||
|
instance => actor.id?.includes(instance)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
rawActor.data.image = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await rawActor.isObjectFiltered()) {
|
||||||
|
return errorResponse("Actor filtered", 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
await rawActor.save();
|
||||||
|
this.actors.push(rawActor);
|
||||||
|
return rawActor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ export class RawActor extends BaseEntity {
|
||||||
@Column("jsonb")
|
@Column("jsonb")
|
||||||
data!: APActor;
|
data!: APActor;
|
||||||
|
|
||||||
@Column("jsonb")
|
@Column("jsonb", {
|
||||||
|
default: [],
|
||||||
|
})
|
||||||
followers!: string[];
|
followers!: string[];
|
||||||
|
|
||||||
static async getByActorId(id: string) {
|
static async getByActorId(id: string) {
|
||||||
|
|
@ -33,6 +35,7 @@ export class RawActor extends BaseEntity {
|
||||||
if (!(await RawActor.exists(data.id ?? ""))) {
|
if (!(await RawActor.exists(data.id ?? ""))) {
|
||||||
const actor = new RawActor();
|
const actor = new RawActor();
|
||||||
actor.data = data;
|
actor.data = data;
|
||||||
|
actor.followers = [];
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@ export class User extends BaseEntity {
|
||||||
user.avatar = data.avatar ?? config.defaults.avatar;
|
user.avatar = data.avatar ?? config.defaults.avatar;
|
||||||
user.header = data.header ?? config.defaults.avatar;
|
user.header = data.header ?? config.defaults.avatar;
|
||||||
|
|
||||||
|
user.followers = [];
|
||||||
|
user.following = [];
|
||||||
|
|
||||||
await user.generateKeys();
|
await user.generateKeys();
|
||||||
await user.updateActor();
|
await user.updateActor();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,10 @@ export default async (
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activity = await RawActivity.addIfNotExists(body);
|
const activity = await RawActivity.addIfNotExists(body, object);
|
||||||
|
|
||||||
if (activity instanceof Response) {
|
if (activity instanceof Response) {
|
||||||
|
console.log(await activity.text());
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,7 +79,13 @@ export default async (
|
||||||
// Delete the object from database
|
// Delete the object from database
|
||||||
// TODO: Add authentication
|
// TODO: Add authentication
|
||||||
|
|
||||||
await RawActivity.deleteObjectIfExists(body.object as APObject);
|
const response = await RawActivity.deleteObjectIfExists(
|
||||||
|
body.object as APObject
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response instanceof Response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
// Store the Delete event in the database
|
// Store the Delete event in the database
|
||||||
const activity = await RawActivity.addIfNotExists(body);
|
const activity = await RawActivity.addIfNotExists(body);
|
||||||
|
|
|
||||||
116
server/api/api/v1/statuses/index.ts
Normal file
116
server/api/api/v1/statuses/index.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { getUserByToken } from "@auth";
|
||||||
|
import { getConfig } from "@config";
|
||||||
|
import { parseRequest } from "@request";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { Application } from "~database/entities/Application";
|
||||||
|
import { Status } from "~database/entities/Status";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post new status
|
||||||
|
*/
|
||||||
|
export default async (req: Request): Promise<Response> => {
|
||||||
|
// Check if request is a PATCH request
|
||||||
|
if (req.method !== "POST")
|
||||||
|
return errorResponse("This method requires a POST request", 405);
|
||||||
|
|
||||||
|
// Check auth token
|
||||||
|
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||||
|
|
||||||
|
if (!token)
|
||||||
|
return errorResponse("This method requires an authenticated user", 422);
|
||||||
|
|
||||||
|
const user = await getUserByToken(token);
|
||||||
|
const application = await Application.getFromToken(token);
|
||||||
|
|
||||||
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
const {
|
||||||
|
status,
|
||||||
|
media_ids,
|
||||||
|
"poll[expires_in]": expires_in,
|
||||||
|
"poll[hide_totals]": hide_totals,
|
||||||
|
"poll[multiple]": multiple,
|
||||||
|
"poll[options]": options,
|
||||||
|
in_reply_to_id,
|
||||||
|
language,
|
||||||
|
scheduled_at,
|
||||||
|
sensitive,
|
||||||
|
spoiler_text,
|
||||||
|
visibility,
|
||||||
|
} = await parseRequest<{
|
||||||
|
status: string;
|
||||||
|
media_ids?: string[];
|
||||||
|
"poll[options]"?: string[];
|
||||||
|
"poll[expires_in]"?: number;
|
||||||
|
"poll[multiple]"?: boolean;
|
||||||
|
"poll[hide_totals]"?: boolean;
|
||||||
|
in_reply_to_id?: string;
|
||||||
|
sensitive?: boolean;
|
||||||
|
spoiler_text?: string;
|
||||||
|
visibility?: "public" | "unlisted" | "private" | "direct";
|
||||||
|
language?: string;
|
||||||
|
scheduled_at?: string;
|
||||||
|
}>(req);
|
||||||
|
|
||||||
|
// Validate status
|
||||||
|
if (!status) {
|
||||||
|
return errorResponse("Status is required", 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.length > config.validation.max_note_size) {
|
||||||
|
return errorResponse(
|
||||||
|
`Status must be less than ${config.validation.max_note_size} characters`,
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate media_ids
|
||||||
|
if (media_ids && !Array.isArray(media_ids)) {
|
||||||
|
return errorResponse("Media IDs must be an array", 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate poll options
|
||||||
|
if (options && !Array.isArray(options)) {
|
||||||
|
return errorResponse("Poll options must be an array", 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options && options.length > 4) {
|
||||||
|
return errorResponse("Poll options must be less than 5", 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate poll expires_in
|
||||||
|
if (expires_in && (expires_in < 60 || expires_in > 604800)) {
|
||||||
|
return errorResponse(
|
||||||
|
"Poll expires_in must be between 60 and 604800",
|
||||||
|
422
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate visibility
|
||||||
|
if (
|
||||||
|
visibility &&
|
||||||
|
!["public", "unlisted", "private", "direct"].includes(visibility)
|
||||||
|
) {
|
||||||
|
return errorResponse("Invalid visibility", 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create status
|
||||||
|
const newStatus = new Status();
|
||||||
|
newStatus.account = user;
|
||||||
|
newStatus.application = application;
|
||||||
|
newStatus.content = status;
|
||||||
|
newStatus.spoiler_text = spoiler_text || "";
|
||||||
|
newStatus.sensitive = sensitive || false;
|
||||||
|
newStatus.visibility = visibility || "public";
|
||||||
|
newStatus.likes = [];
|
||||||
|
newStatus.isReblog = false;
|
||||||
|
|
||||||
|
// TODO: add database jobs to deliver the post
|
||||||
|
|
||||||
|
await newStatus.save();
|
||||||
|
|
||||||
|
return jsonResponse(newStatus);
|
||||||
|
};
|
||||||
103
tests/actor.test.ts
Normal file
103
tests/actor.test.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { getConfig } from "@config";
|
||||||
|
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 { Token } from "~database/entities/Token";
|
||||||
|
import { User } from "~database/entities/User";
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
||||||
|
|
||||||
|
// Initialize test user
|
||||||
|
await User.createNew({
|
||||||
|
email: "test@test.com",
|
||||||
|
username: "test",
|
||||||
|
password: "test",
|
||||||
|
display_name: "",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("POST /@test", () => {
|
||||||
|
test("should return a valid ActivityPub Actor when querying an existing user", async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
`${config.http.base_url}:${config.http.port}/@test`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/activity+json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.headers.get("content-type")).toBe(
|
||||||
|
"application/activity+json"
|
||||||
|
);
|
||||||
|
|
||||||
|
const actor: APActor = await response.json();
|
||||||
|
|
||||||
|
expect(actor.type).toBe("Person");
|
||||||
|
expect(actor.id).toBe(
|
||||||
|
`${config.http.base_url}:${config.http.port}/@test`
|
||||||
|
);
|
||||||
|
expect(actor.preferredUsername).toBe("test");
|
||||||
|
expect(actor.inbox).toBe(
|
||||||
|
`${config.http.base_url}:${config.http.port}/@test/inbox`
|
||||||
|
);
|
||||||
|
expect(actor.outbox).toBe(
|
||||||
|
`${config.http.base_url}:${config.http.port}/@test/outbox`
|
||||||
|
);
|
||||||
|
expect(actor.followers).toBe(
|
||||||
|
`${config.http.base_url}:${config.http.port}/@test/followers`
|
||||||
|
);
|
||||||
|
expect(actor.following).toBe(
|
||||||
|
`${config.http.base_url}:${config.http.port}/@test/following`
|
||||||
|
);
|
||||||
|
expect((actor as any).publicKey).toBeDefined();
|
||||||
|
expect((actor as any).publicKey.id).toBeDefined();
|
||||||
|
expect((actor as any).publicKey.owner).toBe(
|
||||||
|
`${config.http.base_url}:${config.http.port}/@test`
|
||||||
|
);
|
||||||
|
expect((actor as any).publicKey.publicKeyPem).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// Clean up user
|
||||||
|
const user = await User.findOneBy({
|
||||||
|
username: "test",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up tokens
|
||||||
|
const tokens = await Token.findBy({
|
||||||
|
user: {
|
||||||
|
username: "test",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const activities = await RawActivity.createQueryBuilder("activity")
|
||||||
|
.where("activity.data->>'actor' = :actor", {
|
||||||
|
actor: `${config.http.base_url}:${config.http.port}/@test`,
|
||||||
|
})
|
||||||
|
.leftJoinAndSelect("activity.objects", "objects")
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
// Delete all created objects and activities as part of testing
|
||||||
|
await Promise.all(
|
||||||
|
activities.map(async activity => {
|
||||||
|
await Promise.all(
|
||||||
|
activity.objects.map(async object => await object.remove())
|
||||||
|
);
|
||||||
|
await activity.remove();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(tokens.map(async token => await token.remove()));
|
||||||
|
|
||||||
|
if (user) await user.remove();
|
||||||
|
});
|
||||||
189
tests/api.test.ts
Normal file
189
tests/api.test.ts
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { getConfig } from "@config";
|
||||||
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
|
import { AppDataSource } from "~database/datasource";
|
||||||
|
import { Application } from "~database/entities/Application";
|
||||||
|
import { RawActivity } from "~database/entities/RawActivity";
|
||||||
|
import { Token, TokenType } from "~database/entities/Token";
|
||||||
|
import { User } from "~database/entities/User";
|
||||||
|
import { APIAccount } from "~types/entities/account";
|
||||||
|
import { APIStatus } from "~types/entities/status";
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
let token: Token;
|
||||||
|
let user: User;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
||||||
|
|
||||||
|
// Initialize test user
|
||||||
|
user = await User.createNew({
|
||||||
|
email: "test@test.com",
|
||||||
|
username: "test",
|
||||||
|
password: "test",
|
||||||
|
display_name: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = new Application();
|
||||||
|
|
||||||
|
app.name = "Test Application";
|
||||||
|
app.website = "https://example.com";
|
||||||
|
app.client_id = "test";
|
||||||
|
app.redirect_uris = "https://example.com";
|
||||||
|
app.scopes = "read write";
|
||||||
|
app.secret = "test";
|
||||||
|
app.vapid_key = null;
|
||||||
|
|
||||||
|
await app.save();
|
||||||
|
|
||||||
|
// Initialize test token
|
||||||
|
token = new Token();
|
||||||
|
|
||||||
|
token.access_token = "test";
|
||||||
|
token.application = app;
|
||||||
|
token.code = "test";
|
||||||
|
token.scope = "read write";
|
||||||
|
token.token_type = TokenType.BEARER;
|
||||||
|
token.user = user;
|
||||||
|
|
||||||
|
await token.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("POST /api/v1/accounts/:id", () => {
|
||||||
|
test("should return a 404 error when trying to update a non-existent user", async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
`${config.http.base_url}:${config.http.port}/api/v1/accounts/999999`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token.access_token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
expect(response.headers.get("content-type")).toBe("application/json");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("POST /api/v1/statuses", () => {
|
||||||
|
test("should create a new status and return an APIStatus object", async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
`${config.http.base_url}:${config.http.port}/api/v1/statuses`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token.access_token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
status: "Hello, world!",
|
||||||
|
visibility: "public",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.headers.get("content-type")).toBe("application/json");
|
||||||
|
|
||||||
|
const status: APIStatus = await response.json();
|
||||||
|
|
||||||
|
expect(status.content).toBe("Hello, world!");
|
||||||
|
expect(status.visibility).toBe("public");
|
||||||
|
expect(status.account.id).toBe(
|
||||||
|
`${config.http.base_url}:${config.http.port}/@test`
|
||||||
|
);
|
||||||
|
expect(status.replies_count).toBe(0);
|
||||||
|
expect(status.favourites_count).toBe(0);
|
||||||
|
expect(status.reblogged).toBe(false);
|
||||||
|
expect(status.favourited).toBe(false);
|
||||||
|
expect(status.reblog?.content).toBe("Hello, world!");
|
||||||
|
expect(status.reblog?.visibility).toBe("public");
|
||||||
|
expect(status.reblog?.account.id).toBe(
|
||||||
|
`${config.http.base_url}:${config.http.port}/@test`
|
||||||
|
);
|
||||||
|
expect(status.reblog?.replies_count).toBe(0);
|
||||||
|
expect(status.reblog?.favourites_count).toBe(0);
|
||||||
|
expect(status.reblog?.reblogged).toBe(false);
|
||||||
|
expect(status.reblog?.favourited).toBe(false);
|
||||||
|
expect(status.media_attachments).toEqual([]);
|
||||||
|
expect(status.mentions).toEqual([]);
|
||||||
|
expect(status.tags).toEqual([]);
|
||||||
|
expect(status.application).toBeNull();
|
||||||
|
expect(status.sensitive).toBe(false);
|
||||||
|
expect(status.spoiler_text).toBe("");
|
||||||
|
expect(status.language).toBeNull();
|
||||||
|
expect(status.pinned).toBe(false);
|
||||||
|
expect(status.visibility).toBe("public");
|
||||||
|
expect(status.card).toBeNull();
|
||||||
|
expect(status.poll).toBeNull();
|
||||||
|
expect(status.emojis).toEqual([]);
|
||||||
|
expect(status.in_reply_to_id).toBeNull();
|
||||||
|
expect(status.in_reply_to_account_id).toBeNull();
|
||||||
|
expect(status.reblog?.in_reply_to_id).toBeNull();
|
||||||
|
expect(status.reblog?.in_reply_to_account_id).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("POST /api/v1/accounts/update_credentials", () => {
|
||||||
|
test("should update the authenticated user's display name", async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
`${config.http.base_url}:${config.http.port}/api/v1/accounts/update_credentials`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token.access_token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
display_name: "New Display Name",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.headers.get("content-type")).toBe("application/json");
|
||||||
|
|
||||||
|
const user: APIAccount = await response.json();
|
||||||
|
|
||||||
|
expect(user.display_name).toBe("New Display Name");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// Clean up user
|
||||||
|
const user = await User.findOneBy({
|
||||||
|
username: "test",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up tokens
|
||||||
|
const tokens = await Token.findBy({
|
||||||
|
user: {
|
||||||
|
username: "test",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const activities = await RawActivity.createQueryBuilder("activity")
|
||||||
|
.where("activity.data->>'actor' = :actor", {
|
||||||
|
actor: `${config.http.base_url}:${config.http.port}/@test`,
|
||||||
|
})
|
||||||
|
.leftJoinAndSelect("activity.objects", "objects")
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
// Delete all created objects and activities as part of testing
|
||||||
|
await Promise.all(
|
||||||
|
activities.map(async activity => {
|
||||||
|
await Promise.all(
|
||||||
|
activity.objects.map(async object => await object.remove())
|
||||||
|
);
|
||||||
|
await activity.remove();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(tokens.map(async token => await token.remove()));
|
||||||
|
|
||||||
|
if (user) await user.remove();
|
||||||
|
});
|
||||||
|
|
@ -11,21 +11,18 @@ beforeAll(async () => {
|
||||||
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
||||||
|
|
||||||
// Initialize test user
|
// Initialize test user
|
||||||
const user = new User();
|
await User.createNew({
|
||||||
|
email: "test@test.com",
|
||||||
user.email = "test@test.com";
|
username: "test",
|
||||||
user.username = "test";
|
password: "test",
|
||||||
user.password = await Bun.password.hash("test");
|
display_name: "",
|
||||||
user.display_name = "";
|
});
|
||||||
user.note = "";
|
|
||||||
|
|
||||||
await user.generateKeys();
|
|
||||||
|
|
||||||
await user.save();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("POST /@test/inbox", () => {
|
describe("POST /@test/inbox", () => {
|
||||||
test("should store a new Note object", async () => {
|
test("should store a new Note object", async () => {
|
||||||
|
const activityId = `https://example.com/objects/${crypto.randomUUID()}`;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${config.http.base_url}:${config.http.port}/@test/inbox/`,
|
`${config.http.base_url}:${config.http.port}/@test/inbox/`,
|
||||||
{
|
{
|
||||||
|
|
@ -36,8 +33,12 @@ describe("POST /@test/inbox", () => {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
type: "Create",
|
type: "Create",
|
||||||
id: "https://example.com/notes/1/activity",
|
id: activityId,
|
||||||
actor: `${config.http.base_url}:${config.http.port}/@test`,
|
actor: {
|
||||||
|
id: `${config.http.base_url}:${config.http.port}/@test`,
|
||||||
|
type: "Person",
|
||||||
|
preferredUsername: "test",
|
||||||
|
},
|
||||||
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
cc: [],
|
cc: [],
|
||||||
published: "2021-01-01T00:00:00.000Z",
|
published: "2021-01-01T00:00:00.000Z",
|
||||||
|
|
@ -57,16 +58,13 @@ describe("POST /@test/inbox", () => {
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.headers.get("content-type")).toBe("application/json");
|
expect(response.headers.get("content-type")).toBe("application/json");
|
||||||
|
|
||||||
const activity = await RawActivity.getLatestById(
|
const activity = await RawActivity.getLatestById(activityId);
|
||||||
"https://example.com/notes/1/activity"
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(activity).not.toBeUndefined();
|
expect(activity).not.toBeUndefined();
|
||||||
expect(activity?.data).toEqual({
|
expect(activity?.data).toEqual({
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
type: "Create",
|
type: "Create",
|
||||||
id: "https://example.com/notes/1/activity",
|
id: activityId,
|
||||||
actor: `${config.http.base_url}:${config.http.port}/@test`,
|
|
||||||
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
cc: [],
|
cc: [],
|
||||||
published: "2021-01-01T00:00:00.000Z",
|
published: "2021-01-01T00:00:00.000Z",
|
||||||
|
|
@ -85,6 +83,8 @@ describe("POST /@test/inbox", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should try to update that Note object", async () => {
|
test("should try to update that Note object", async () => {
|
||||||
|
const activityId = `https://example.com/objects/${crypto.randomUUID()}`;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${config.http.base_url}:${config.http.port}/@test/inbox/`,
|
`${config.http.base_url}:${config.http.port}/@test/inbox/`,
|
||||||
{
|
{
|
||||||
|
|
@ -95,8 +95,12 @@ describe("POST /@test/inbox", () => {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
type: "Update",
|
type: "Update",
|
||||||
id: "https://example.com/notes/1/activity",
|
id: activityId,
|
||||||
actor: `${config.http.base_url}:${config.http.port}/@test`,
|
actor: {
|
||||||
|
id: `${config.http.base_url}:${config.http.port}/@test`,
|
||||||
|
type: "Person",
|
||||||
|
preferredUsername: "test",
|
||||||
|
},
|
||||||
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
cc: [],
|
cc: [],
|
||||||
published: "2021-01-02T00:00:00.000Z",
|
published: "2021-01-02T00:00:00.000Z",
|
||||||
|
|
@ -116,16 +120,13 @@ describe("POST /@test/inbox", () => {
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.headers.get("content-type")).toBe("application/json");
|
expect(response.headers.get("content-type")).toBe("application/json");
|
||||||
|
|
||||||
const activity = await RawActivity.getLatestById(
|
const activity = await RawActivity.getLatestById(activityId);
|
||||||
"https://example.com/notes/1/activity"
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(activity).not.toBeUndefined();
|
expect(activity).not.toBeUndefined();
|
||||||
expect(activity?.data).toEqual({
|
expect(activity?.data).toEqual({
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
type: "Update",
|
type: "Update",
|
||||||
id: "https://example.com/notes/1/activity",
|
id: activityId,
|
||||||
actor: `${config.http.base_url}:${config.http.port}/@test`,
|
|
||||||
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
cc: [],
|
cc: [],
|
||||||
published: "2021-01-02T00:00:00.000Z",
|
published: "2021-01-02T00:00:00.000Z",
|
||||||
|
|
@ -144,6 +145,7 @@ describe("POST /@test/inbox", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should delete the Note object", async () => {
|
test("should delete the Note object", async () => {
|
||||||
|
const activityId = `https://example.com/objects/${crypto.randomUUID()}`;
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${config.http.base_url}:${config.http.port}/@test/inbox/`,
|
`${config.http.base_url}:${config.http.port}/@test/inbox/`,
|
||||||
{
|
{
|
||||||
|
|
@ -154,8 +156,12 @@ describe("POST /@test/inbox", () => {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
type: "Delete",
|
type: "Delete",
|
||||||
id: "https://example.com/notes/1/activity",
|
id: activityId,
|
||||||
actor: `${config.http.base_url}:${config.http.port}/@test`,
|
actor: {
|
||||||
|
id: `${config.http.base_url}:${config.http.port}/@test`,
|
||||||
|
type: "Person",
|
||||||
|
preferredUsername: "test",
|
||||||
|
},
|
||||||
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
cc: [],
|
cc: [],
|
||||||
published: "2021-01-03T00:00:00.000Z",
|
published: "2021-01-03T00:00:00.000Z",
|
||||||
|
|
@ -175,21 +181,25 @@ describe("POST /@test/inbox", () => {
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.headers.get("content-type")).toBe("application/json");
|
expect(response.headers.get("content-type")).toBe("application/json");
|
||||||
|
|
||||||
const activity = await RawActivity.getLatestById(
|
const activity = await RawActivity.getLatestById(activityId);
|
||||||
"https://example.com/notes/1/activity"
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(activity).not.toBeUndefined();
|
expect(activity).not.toBeUndefined();
|
||||||
expect(activity?.data).toEqual({
|
expect(activity?.data).toEqual({
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
type: "Delete",
|
type: "Delete",
|
||||||
id: "https://example.com/notes/1/activity",
|
id: activityId,
|
||||||
actor: `${config.http.base_url}:${config.http.port}/@test`,
|
|
||||||
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
cc: [],
|
cc: [],
|
||||||
published: "2021-01-03T00:00:00.000Z",
|
published: "2021-01-03T00:00:00.000Z",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(activity?.actors).toHaveLength(1);
|
||||||
|
expect(activity?.actors[0].data).toEqual({
|
||||||
|
preferredUsername: "test",
|
||||||
|
id: `${config.http.base_url}:${config.http.port}/@test`,
|
||||||
|
type: "Person",
|
||||||
|
});
|
||||||
|
|
||||||
// Can be 0 or 1 length depending on whether config.activitypub.use_tombstone is true or false
|
// Can be 0 or 1 length depending on whether config.activitypub.use_tombstone is true or false
|
||||||
if (config.activitypub.use_tombstones) {
|
if (config.activitypub.use_tombstones) {
|
||||||
expect(activity?.objects).toHaveLength(1);
|
expect(activity?.objects).toHaveLength(1);
|
||||||
|
|
@ -197,6 +207,41 @@ describe("POST /@test/inbox", () => {
|
||||||
expect(activity?.objects).toHaveLength(0);
|
expect(activity?.objects).toHaveLength(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should return a 404 error when trying to delete a non-existent Note object", async () => {
|
||||||
|
const activityId = `https://example.com/objects/${crypto.randomUUID()}`;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${config.http.base_url}:${config.http.port}/@test/inbox/`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/activity+json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
type: "Delete",
|
||||||
|
id: activityId,
|
||||||
|
actor: {
|
||||||
|
id: `${config.http.base_url}:${config.http.port}/@test`,
|
||||||
|
type: "Person",
|
||||||
|
preferredUsername: "test",
|
||||||
|
},
|
||||||
|
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
cc: [],
|
||||||
|
published: "2021-01-03T00:00:00.000Z",
|
||||||
|
object: {
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
id: "https://example.com/notes/2345678909876543",
|
||||||
|
type: "Note",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
expect(response.headers.get("content-type")).toBe("application/json");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|
@ -213,10 +258,16 @@ afterAll(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const activities = await RawActivity.createQueryBuilder("activity")
|
const activities = await RawActivity.createQueryBuilder("activity")
|
||||||
.where("activity.data->>'actor' = :actor", {
|
// Join objects
|
||||||
actor: `${config.http.base_url}:${config.http.port}/@test`,
|
|
||||||
})
|
|
||||||
.leftJoinAndSelect("activity.objects", "objects")
|
.leftJoinAndSelect("activity.objects", "objects")
|
||||||
|
.leftJoinAndSelect("activity.actors", "actors")
|
||||||
|
// activity.actors is a many-to-many relationship with Actor objects (it is an array of Actor objects)
|
||||||
|
// Get the actors of the activity that have data.id as `${config.http.base_url}:${config.http.port}/@test`
|
||||||
|
.where("actors.data @> :data", {
|
||||||
|
data: JSON.stringify({
|
||||||
|
id: `${config.http.base_url}:${config.http.port}/@test`,
|
||||||
|
}),
|
||||||
|
})
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
// Delete all created objects and activities as part of testing
|
// Delete all created objects and activities as part of testing
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,12 @@ beforeAll(async () => {
|
||||||
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
||||||
|
|
||||||
// Initialize test user
|
// Initialize test user
|
||||||
const user = new User();
|
await User.createNew({
|
||||||
|
email: "test@test.com",
|
||||||
user.email = "test@test.com";
|
username: "test",
|
||||||
user.username = "test";
|
password: "test",
|
||||||
user.password = await Bun.password.hash("test");
|
display_name: "",
|
||||||
user.display_name = "";
|
});
|
||||||
user.note = "";
|
|
||||||
|
|
||||||
await user.generateKeys();
|
|
||||||
|
|
||||||
await user.save();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("POST /v1/apps/", () => {
|
describe("POST /v1/apps/", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue