mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
refactor: ✅ Refactor tests to not use module mocks, so bun test can be used
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 45s
Build Docker Images / lint (push) Successful in 27s
Build Docker Images / check (push) Successful in 1m7s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 12s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 32m31s
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 45s
Build Docker Images / lint (push) Successful in 27s
Build Docker Images / check (push) Successful in 1m7s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 12s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 32m31s
This commit is contained in:
parent
ec506241f0
commit
7112a66e4c
|
|
@ -15,7 +15,7 @@ beforeAll(async () => {
|
||||||
priority: 2,
|
priority: 2,
|
||||||
description: "test",
|
description: "test",
|
||||||
visible: true,
|
visible: true,
|
||||||
icon: "test",
|
icon: "https://test.com",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(role).toBeDefined();
|
expect(role).toBeDefined();
|
||||||
|
|
@ -29,7 +29,6 @@ beforeAll(async () => {
|
||||||
priority: 3, // Higher priority than the user's role
|
priority: 3, // Higher priority than the user's role
|
||||||
description: "Higher priority role",
|
description: "Higher priority role",
|
||||||
visible: true,
|
visible: true,
|
||||||
icon: "higherPriorityRole",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(higherPriorityRole).toBeDefined();
|
expect(higherPriorityRole).toBeDefined();
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ beforeAll(async () => {
|
||||||
priority: 3, // Higher priority than the user's role
|
priority: 3, // Higher priority than the user's role
|
||||||
description: "Higher priority role",
|
description: "Higher priority role",
|
||||||
visible: true,
|
visible: true,
|
||||||
icon: "higherPriorityRole",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(higherPriorityRole).toBeDefined();
|
expect(higherPriorityRole).toBeDefined();
|
||||||
|
|
@ -179,7 +178,7 @@ describe("/api/v1/roles/:id", () => {
|
||||||
priority: 2,
|
priority: 2,
|
||||||
description: "test",
|
description: "test",
|
||||||
visible: true,
|
visible: true,
|
||||||
icon: "test",
|
icon: "https://test.com",
|
||||||
});
|
});
|
||||||
|
|
||||||
await using client = await generateClient(users[0]);
|
await using client = await generateClient(users[0]);
|
||||||
|
|
|
||||||
|
|
@ -1,477 +0,0 @@
|
||||||
import { beforeEach, describe, expect, jest, mock, test } from "bun:test";
|
|
||||||
import { SignatureValidator } from "@versia/federation";
|
|
||||||
import type { Entity, Note as VersiaNote } from "@versia/federation/types";
|
|
||||||
import {
|
|
||||||
Instance,
|
|
||||||
Note,
|
|
||||||
Notification,
|
|
||||||
Relationship,
|
|
||||||
User,
|
|
||||||
} from "@versia/kit/db";
|
|
||||||
import type { SocketAddress } from "bun";
|
|
||||||
import type { z } from "zod";
|
|
||||||
import { ValidationError } from "zod-validation-error";
|
|
||||||
import { config } from "~/config.ts";
|
|
||||||
import type { ConfigSchema } from "../config/schema.ts";
|
|
||||||
import { InboxProcessor } from "./processor.ts";
|
|
||||||
|
|
||||||
// Mock dependencies
|
|
||||||
mock.module("@versia/kit/db", () => ({
|
|
||||||
db: {
|
|
||||||
insert: jest.fn(
|
|
||||||
// Return something with a `.values()` method
|
|
||||||
() => ({ values: jest.fn() }),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
User: {
|
|
||||||
resolve: jest.fn(),
|
|
||||||
fetchFromRemote: jest.fn(),
|
|
||||||
sendFollowAccept: jest.fn(),
|
|
||||||
},
|
|
||||||
Instance: {
|
|
||||||
fromUser: jest.fn(),
|
|
||||||
resolve: jest.fn(),
|
|
||||||
},
|
|
||||||
Note: {
|
|
||||||
resolve: jest.fn(),
|
|
||||||
fromVersia: jest.fn(),
|
|
||||||
fromSql: jest.fn(),
|
|
||||||
},
|
|
||||||
Relationship: {
|
|
||||||
fromOwnerAndSubject: jest.fn(),
|
|
||||||
},
|
|
||||||
Like: {
|
|
||||||
fromSql: jest.fn(),
|
|
||||||
},
|
|
||||||
Notification: {
|
|
||||||
fromSql: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
mock.module("@versia/federation", () => ({
|
|
||||||
SignatureValidator: {
|
|
||||||
fromStringKey: jest.fn(() => ({
|
|
||||||
validate: jest.fn(),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
EntityValidator: jest.fn(() => ({
|
|
||||||
validate: jest.fn(),
|
|
||||||
})),
|
|
||||||
RequestParserHandler: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
mock.module("~/config.ts", () => ({
|
|
||||||
config: {
|
|
||||||
debug: {
|
|
||||||
federation: false,
|
|
||||||
},
|
|
||||||
federation: {
|
|
||||||
blocked: [],
|
|
||||||
bridge: {
|
|
||||||
enabled: false,
|
|
||||||
token: "test-token",
|
|
||||||
allowed_ips: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("InboxProcessor", () => {
|
|
||||||
let mockRequest: {
|
|
||||||
url: URL;
|
|
||||||
method: string;
|
|
||||||
body: string;
|
|
||||||
};
|
|
||||||
let mockBody: Entity;
|
|
||||||
let mockSenderInstance: Instance;
|
|
||||||
let mockHeaders: {
|
|
||||||
signature: string;
|
|
||||||
signedAt: Date;
|
|
||||||
authorization?: string;
|
|
||||||
};
|
|
||||||
let processor: InboxProcessor;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Reset all mocks
|
|
||||||
mock.restore();
|
|
||||||
|
|
||||||
// Setup basic mock context
|
|
||||||
mockRequest = {
|
|
||||||
url: new URL("https://test.com"),
|
|
||||||
method: "POST",
|
|
||||||
body: "test-body",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup basic mock sender
|
|
||||||
mockSenderInstance = {
|
|
||||||
id: "test-id",
|
|
||||||
data: {
|
|
||||||
publicKey: {
|
|
||||||
key: "test-key",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as Instance;
|
|
||||||
|
|
||||||
// Setup basic mock headers
|
|
||||||
mockHeaders = {
|
|
||||||
signature: "test-signature",
|
|
||||||
signedAt: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup basic mock body
|
|
||||||
mockBody = {} as Entity;
|
|
||||||
|
|
||||||
// Create processor instance
|
|
||||||
processor = new InboxProcessor(
|
|
||||||
mockRequest,
|
|
||||||
mockBody,
|
|
||||||
{
|
|
||||||
instance: mockSenderInstance,
|
|
||||||
key: "test-key",
|
|
||||||
},
|
|
||||||
mockHeaders,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
address: "127.0.0.1",
|
|
||||||
} as SocketAddress,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("isSignatureValid", () => {
|
|
||||||
test("returns true for valid signature", async () => {
|
|
||||||
const mockValidator = {
|
|
||||||
validate: jest.fn().mockResolvedValue(true),
|
|
||||||
};
|
|
||||||
SignatureValidator.fromStringKey = jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValue(mockValidator);
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = await processor["isSignatureValid"]();
|
|
||||||
expect(result).toBe(true);
|
|
||||||
expect(mockValidator.validate).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns false for invalid signature", async () => {
|
|
||||||
const mockValidator = {
|
|
||||||
validate: jest.fn().mockResolvedValue(false),
|
|
||||||
};
|
|
||||||
SignatureValidator.fromStringKey = jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValue(mockValidator);
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = await processor["isSignatureValid"]();
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("shouldCheckSignature", () => {
|
|
||||||
test("returns true when bridge is disabled", () => {
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = processor["shouldCheckSignature"]();
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns false for valid bridge request", () => {
|
|
||||||
config.federation.bridge = {
|
|
||||||
token: "valid-token",
|
|
||||||
allowed_ips: ["127.0.0.1"],
|
|
||||||
url: new URL("https://test.com"),
|
|
||||||
software: "versia-ap",
|
|
||||||
};
|
|
||||||
|
|
||||||
mockHeaders.authorization = "Bearer valid-token";
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = processor["shouldCheckSignature"]();
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns error response for invalid token", () => {
|
|
||||||
config.federation.bridge = {} as z.infer<
|
|
||||||
typeof ConfigSchema
|
|
||||||
>["federation"]["bridge"];
|
|
||||||
mockHeaders.authorization = "Bearer invalid-token";
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = processor["shouldCheckSignature"]() as {
|
|
||||||
code: number;
|
|
||||||
};
|
|
||||||
expect(result.code).toBe(401);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("processNote", () => {
|
|
||||||
test("successfully processes valid note", async () => {
|
|
||||||
const mockNote = {
|
|
||||||
author: "https://example.com",
|
|
||||||
uri: "https://note.example.com",
|
|
||||||
};
|
|
||||||
const mockAuthor = { id: "test-id" };
|
|
||||||
const mockInstance = { id: "test-id" };
|
|
||||||
|
|
||||||
User.resolve = jest.fn().mockResolvedValue(mockAuthor);
|
|
||||||
Note.fromVersia = jest.fn().mockResolvedValue(true);
|
|
||||||
Instance.resolve = jest.fn().mockResolvedValue(mockInstance);
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private variable
|
|
||||||
processor["body"] = mockNote as VersiaNote;
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = await processor["processNote"]();
|
|
||||||
|
|
||||||
expect(User.resolve).toHaveBeenCalledWith(
|
|
||||||
new URL("https://example.com"),
|
|
||||||
);
|
|
||||||
expect(Note.fromVersia).toHaveBeenCalledWith(
|
|
||||||
mockNote,
|
|
||||||
mockAuthor,
|
|
||||||
mockInstance,
|
|
||||||
);
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns 404 when author not found", async () => {
|
|
||||||
User.resolve = jest.fn().mockResolvedValue(null);
|
|
||||||
const mockNote = {
|
|
||||||
author: "https://example.com",
|
|
||||||
uri: "https://note.example.com",
|
|
||||||
};
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private variable
|
|
||||||
processor["body"] = mockNote as VersiaNote;
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = await processor["processNote"]();
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
Response.json({ error: "Author not found" }, { status: 404 }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("processFollowRequest", () => {
|
|
||||||
test("successfully processes follow request for unlocked account", async () => {
|
|
||||||
const mockFollow = {
|
|
||||||
author: "https://example.com",
|
|
||||||
followee: "https://followee.note.com",
|
|
||||||
};
|
|
||||||
const mockAuthor = { id: "author-id" };
|
|
||||||
const mockFollowee = {
|
|
||||||
id: "followee-id",
|
|
||||||
data: { isLocked: false },
|
|
||||||
sendFollowAccept: jest.fn(),
|
|
||||||
notify: jest.fn(),
|
|
||||||
};
|
|
||||||
const mockRelationship = {
|
|
||||||
data: { following: false },
|
|
||||||
update: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
User.resolve = jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce(mockAuthor)
|
|
||||||
.mockResolvedValueOnce(mockFollowee);
|
|
||||||
Relationship.fromOwnerAndSubject = jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValue(mockRelationship);
|
|
||||||
Notification.insert = jest.fn();
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private variable
|
|
||||||
processor["body"] = mockFollow as unknown as Entity;
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
await processor["processFollowRequest"]();
|
|
||||||
|
|
||||||
expect(mockRelationship.update).toHaveBeenCalledWith({
|
|
||||||
following: true,
|
|
||||||
requested: false,
|
|
||||||
showingReblogs: true,
|
|
||||||
notifying: true,
|
|
||||||
languages: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns 404 when author not found", async () => {
|
|
||||||
User.resolve = jest.fn().mockResolvedValue(null);
|
|
||||||
const mockFollow = {
|
|
||||||
author: "https://example.com",
|
|
||||||
followee: "https://followee.note.com",
|
|
||||||
};
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private variable
|
|
||||||
processor["body"] = mockFollow as unknown as Entity;
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = await processor["processFollowRequest"]();
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
Response.json({ error: "Author not found" }, { status: 404 }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("processDelete", () => {
|
|
||||||
test("successfully deletes a note", async () => {
|
|
||||||
const mockDelete = {
|
|
||||||
deleted_type: "Note",
|
|
||||||
deleted: "https://example.com",
|
|
||||||
};
|
|
||||||
const mockNote = {
|
|
||||||
delete: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Note.fromSql = jest.fn().mockResolvedValue(mockNote);
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private variable
|
|
||||||
processor["body"] = mockDelete as unknown as Entity;
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = await processor["processDelete"]();
|
|
||||||
|
|
||||||
expect(mockNote.delete).toHaveBeenCalled();
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns 404 when note not found", async () => {
|
|
||||||
const mockDelete = {
|
|
||||||
deleted_type: "Note",
|
|
||||||
deleted: "https://example.com",
|
|
||||||
};
|
|
||||||
|
|
||||||
Note.fromSql = jest.fn().mockResolvedValue(null);
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private variable
|
|
||||||
processor["body"] = mockDelete as unknown as Entity;
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = await processor["processDelete"]();
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
Response.json(
|
|
||||||
{
|
|
||||||
error: "Note to delete not found or not owned by sender",
|
|
||||||
},
|
|
||||||
{ status: 404 },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("processLikeRequest", () => {
|
|
||||||
test("successfully processes like request", async () => {
|
|
||||||
const mockLike = {
|
|
||||||
author: "https://example.com",
|
|
||||||
liked: "https://example.note.com",
|
|
||||||
uri: "https://example.com",
|
|
||||||
};
|
|
||||||
const mockAuthor = {
|
|
||||||
like: jest.fn(),
|
|
||||||
};
|
|
||||||
const mockNote = { id: "note-id" };
|
|
||||||
|
|
||||||
User.resolve = jest.fn().mockResolvedValue(mockAuthor);
|
|
||||||
Note.resolve = jest.fn().mockResolvedValue(mockNote);
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private variable
|
|
||||||
processor["body"] = mockLike as unknown as Entity;
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = await processor["processLikeRequest"]();
|
|
||||||
|
|
||||||
expect(mockAuthor.like).toHaveBeenCalledWith(
|
|
||||||
mockNote,
|
|
||||||
"https://example.com",
|
|
||||||
);
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns 404 when author not found", async () => {
|
|
||||||
User.resolve = jest.fn().mockResolvedValue(null);
|
|
||||||
const mockLike = {
|
|
||||||
author: "https://example.com",
|
|
||||||
liked: "https://example.note.com",
|
|
||||||
uri: "https://example.com",
|
|
||||||
};
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private variable
|
|
||||||
processor["body"] = mockLike as unknown as Entity;
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = await processor["processLikeRequest"]();
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
Response.json({ error: "Author not found" }, { status: 404 }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("processUserRequest", () => {
|
|
||||||
test("successfully processes user update", async () => {
|
|
||||||
const mockUser = {
|
|
||||||
uri: "https://example.com",
|
|
||||||
};
|
|
||||||
const mockUpdatedUser = { id: "user-id" };
|
|
||||||
|
|
||||||
User.fetchFromRemote = jest.fn().mockResolvedValue(mockUpdatedUser);
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private variable
|
|
||||||
processor["body"] = mockUser as unknown as Entity;
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = await processor["processUserRequest"]();
|
|
||||||
|
|
||||||
expect(User.fetchFromRemote).toHaveBeenCalledWith(
|
|
||||||
new URL("https://example.com"),
|
|
||||||
);
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns 500 when update fails", async () => {
|
|
||||||
const mockUser = {
|
|
||||||
uri: "https://example.com",
|
|
||||||
};
|
|
||||||
User.fetchFromRemote = jest.fn().mockResolvedValue(null);
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private variable
|
|
||||||
processor["body"] = mockUser as unknown as Entity;
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = await processor["processUserRequest"]();
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
Response.json(
|
|
||||||
{ error: "Failed to update user" },
|
|
||||||
{ status: 500 },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("handleError", () => {
|
|
||||||
test("handles validation errors", () => {
|
|
||||||
const validationError = new ValidationError("Invalid data");
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = processor["handleError"](validationError);
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
Response.json(
|
|
||||||
{
|
|
||||||
error: "Failed to process request",
|
|
||||||
error_description: "Invalid data",
|
|
||||||
},
|
|
||||||
{ status: 400 },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("handles general errors", () => {
|
|
||||||
const error = new Error("Something went wrong");
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const result = processor["handleError"](error);
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
Response.json(
|
|
||||||
{
|
|
||||||
error: "Failed to process request",
|
|
||||||
message: "Something went wrong",
|
|
||||||
},
|
|
||||||
{ status: 500 },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { describe, expect, it, mock } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
import { mockModule } from "~/tests/utils.ts";
|
||||||
import { calculateBlurhash } from "./blurhash.ts";
|
import { calculateBlurhash } from "./blurhash.ts";
|
||||||
|
|
||||||
describe("BlurhashPreprocessor", () => {
|
describe("BlurhashPreprocessor", () => {
|
||||||
|
|
@ -49,7 +50,7 @@ describe("BlurhashPreprocessor", () => {
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
});
|
});
|
||||||
|
|
||||||
mock.module("blurhash", () => ({
|
using __ = await mockModule("blurhash", () => ({
|
||||||
encode: (): void => {
|
encode: (): void => {
|
||||||
throw new Error("Test error");
|
throw new Error("Test error");
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,8 @@
|
||||||
import { beforeEach, describe, expect, it, mock } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import type { config } from "~/config.ts";
|
|
||||||
import { convertImage } from "./image-conversion.ts";
|
import { convertImage } from "./image-conversion.ts";
|
||||||
|
|
||||||
describe("ImageConversionPreprocessor", () => {
|
describe("ImageConversionPreprocessor", () => {
|
||||||
let mockConfig: typeof config;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockConfig = {
|
|
||||||
media: {
|
|
||||||
conversion: {
|
|
||||||
convert_images: true,
|
|
||||||
convert_to: "image/webp",
|
|
||||||
convert_vector: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as typeof config;
|
|
||||||
|
|
||||||
mock.module("~/config.ts", () => ({
|
|
||||||
config: mockConfig,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should convert a JPEG image to WebP", async () => {
|
it("should convert a JPEG image to WebP", async () => {
|
||||||
const inputBuffer = await sharp({
|
const inputBuffer = await sharp({
|
||||||
create: {
|
create: {
|
||||||
|
|
@ -37,7 +18,7 @@ describe("ImageConversionPreprocessor", () => {
|
||||||
const inputFile = new File([inputBuffer], "test.jpg", {
|
const inputFile = new File([inputBuffer], "test.jpg", {
|
||||||
type: "image/jpeg",
|
type: "image/jpeg",
|
||||||
});
|
});
|
||||||
const result = await convertImage(inputFile);
|
const result = await convertImage(inputFile, "image/webp");
|
||||||
|
|
||||||
expect(result.type).toBe("image/webp");
|
expect(result.type).toBe("image/webp");
|
||||||
expect(result.name).toBe("test.webp");
|
expect(result.name).toBe("test.webp");
|
||||||
|
|
@ -53,20 +34,20 @@ describe("ImageConversionPreprocessor", () => {
|
||||||
const inputFile = new File([svgContent], "test.svg", {
|
const inputFile = new File([svgContent], "test.svg", {
|
||||||
type: "image/svg+xml",
|
type: "image/svg+xml",
|
||||||
});
|
});
|
||||||
const result = await convertImage(inputFile);
|
const result = await convertImage(inputFile, "image/webp");
|
||||||
|
|
||||||
expect(result).toBe(inputFile);
|
expect(result).toBe(inputFile);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should convert SVG when convert_vector is true", async () => {
|
it("should convert SVG when convert_vector is true", async () => {
|
||||||
mockConfig.media.conversion.convert_vectors = true;
|
|
||||||
|
|
||||||
const svgContent =
|
const svgContent =
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" fill="red"/></svg>';
|
'<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" fill="red"/></svg>';
|
||||||
const inputFile = new File([svgContent], "test.svg", {
|
const inputFile = new File([svgContent], "test.svg", {
|
||||||
type: "image/svg+xml",
|
type: "image/svg+xml",
|
||||||
});
|
});
|
||||||
const result = await convertImage(inputFile);
|
const result = await convertImage(inputFile, "image/webp", {
|
||||||
|
convertVectors: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.type).toBe("image/webp");
|
expect(result.type).toBe("image/webp");
|
||||||
expect(result.name).toBe("test.webp");
|
expect(result.name).toBe("test.webp");
|
||||||
|
|
@ -76,14 +57,12 @@ describe("ImageConversionPreprocessor", () => {
|
||||||
const inputFile = new File(["test content"], "test.txt", {
|
const inputFile = new File(["test content"], "test.txt", {
|
||||||
type: "text/plain",
|
type: "text/plain",
|
||||||
});
|
});
|
||||||
const result = await convertImage(inputFile);
|
const result = await convertImage(inputFile, "image/webp");
|
||||||
|
|
||||||
expect(result).toBe(inputFile);
|
expect(result).toBe(inputFile);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw an error for unsupported output format", async () => {
|
it("should throw an error for unsupported output format", async () => {
|
||||||
mockConfig.media.conversion.convert_to = "image/bmp";
|
|
||||||
|
|
||||||
const inputBuffer = await sharp({
|
const inputBuffer = await sharp({
|
||||||
create: {
|
create: {
|
||||||
width: 100,
|
width: 100,
|
||||||
|
|
@ -99,7 +78,7 @@ describe("ImageConversionPreprocessor", () => {
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(convertImage(inputFile)).rejects.toThrow(
|
await expect(convertImage(inputFile, "image/bmp")).rejects.toThrow(
|
||||||
"Unsupported output format: image/bmp",
|
"Unsupported output format: image/bmp",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -120,7 +99,7 @@ describe("ImageConversionPreprocessor", () => {
|
||||||
const inputFile = new File([inputBuffer], "animated.gif", {
|
const inputFile = new File([inputBuffer], "animated.gif", {
|
||||||
type: "image/gif",
|
type: "image/gif",
|
||||||
});
|
});
|
||||||
const result = await convertImage(inputFile);
|
const result = await convertImage(inputFile, "image/webp");
|
||||||
|
|
||||||
expect(result.type).toBe("image/webp");
|
expect(result.type).toBe("image/webp");
|
||||||
expect(result.name).toBe("animated.webp");
|
expect(result.name).toBe("animated.webp");
|
||||||
|
|
@ -147,7 +126,7 @@ describe("ImageConversionPreprocessor", () => {
|
||||||
"test image with spaces.png",
|
"test image with spaces.png",
|
||||||
{ type: "image/png" },
|
{ type: "image/png" },
|
||||||
);
|
);
|
||||||
const result = await convertImage(inputFile);
|
const result = await convertImage(inputFile, "image/webp");
|
||||||
|
|
||||||
expect(result.type).toBe("image/webp");
|
expect(result.type).toBe("image/webp");
|
||||||
expect(result.name).toBe("test image with spaces.webp");
|
expect(result.name).toBe("test image with spaces.webp");
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import { config } from "~/config.ts";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supported input media formats.
|
* Supported input media formats.
|
||||||
|
|
@ -36,11 +35,11 @@ const supportedOutputFormats = [
|
||||||
* @param file - The file to check.
|
* @param file - The file to check.
|
||||||
* @returns True if the file is convertible, false otherwise.
|
* @returns True if the file is convertible, false otherwise.
|
||||||
*/
|
*/
|
||||||
const isConvertible = (file: File): boolean => {
|
const isConvertible = (
|
||||||
if (
|
file: File,
|
||||||
file.type === "image/svg+xml" &&
|
options?: { convertVectors?: boolean },
|
||||||
!config.media.conversion.convert_vectors
|
): boolean => {
|
||||||
) {
|
if (file.type === "image/svg+xml" && !options?.convertVectors) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return supportedInputFormats.includes(file.type);
|
return supportedInputFormats.includes(file.type);
|
||||||
|
|
@ -69,14 +68,20 @@ const getReplacedFileName = (fileName: string, newExtension: string): string =>
|
||||||
* Converts an image file to the format specified in the configuration.
|
* Converts an image file to the format specified in the configuration.
|
||||||
*
|
*
|
||||||
* @param file - The image file to convert.
|
* @param file - The image file to convert.
|
||||||
|
* @param targetFormat - The target format to convert to.
|
||||||
* @returns The converted image file.
|
* @returns The converted image file.
|
||||||
*/
|
*/
|
||||||
export const convertImage = async (file: File): Promise<File> => {
|
export const convertImage = async (
|
||||||
if (!isConvertible(file)) {
|
file: File,
|
||||||
|
targetFormat: string,
|
||||||
|
options?: {
|
||||||
|
convertVectors?: boolean;
|
||||||
|
},
|
||||||
|
): Promise<File> => {
|
||||||
|
if (!isConvertible(file, options)) {
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetFormat = config.media.conversion.convert_to;
|
|
||||||
if (!supportedOutputFormats.includes(targetFormat)) {
|
if (!supportedOutputFormats.includes(targetFormat)) {
|
||||||
throw new Error(`Unsupported output format: ${targetFormat}`);
|
throw new Error(`Unsupported output format: ${targetFormat}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {
|
/* import {
|
||||||
afterEach,
|
afterEach,
|
||||||
beforeEach,
|
beforeEach,
|
||||||
describe,
|
describe,
|
||||||
|
|
@ -218,3 +218,4 @@ describe("PluginLoader", () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,14 @@ export const getMediaWorker = (): Worker<MediaJobData, void, MediaJobType> =>
|
||||||
|
|
||||||
await job.log(`Converting attachment [${attachmentId}]`);
|
await job.log(`Converting attachment [${attachmentId}]`);
|
||||||
|
|
||||||
const processedFile = await convertImage(file);
|
const processedFile = await convertImage(
|
||||||
|
file,
|
||||||
|
config.media.conversion.convert_to,
|
||||||
|
{
|
||||||
|
convertVectors:
|
||||||
|
config.media.conversion.convert_vectors,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await job.log(`Uploading attachment [${attachmentId}]`);
|
await job.log(`Uploading attachment [${attachmentId}]`);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
"prune": "ts-prune | grep -v server/ | grep -v dist/ | grep -v '(used in module)'",
|
"prune": "ts-prune | grep -v server/ | grep -v dist/ | grep -v '(used in module)'",
|
||||||
"schema:generate": "bun run classes/config/to-json-schema.ts > config/config.schema.json && bun run packages/plugin-kit/json-schema.ts > packages/plugin-kit/manifest.schema.json",
|
"schema:generate": "bun run classes/config/to-json-schema.ts > config/config.schema.json && bun run packages/plugin-kit/json-schema.ts > packages/plugin-kit/manifest.schema.json",
|
||||||
"check": "bunx tsc -p .",
|
"check": "bunx tsc -p .",
|
||||||
"test": "find . -name \"*.test.ts\" -not -path \"./node_modules/*\" | xargs -I {} sh -c 'bun test {} || exit 255'",
|
"test": "bun test",
|
||||||
"docs:dev": "vitepress dev docs",
|
"docs:dev": "vitepress dev docs",
|
||||||
"docs:build": "vitepress build docs",
|
"docs:build": "vitepress build docs",
|
||||||
"docs:preview": "vitepress preview docs"
|
"docs:preview": "vitepress preview docs"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { mock } from "bun:test";
|
||||||
import { generateChallenge } from "@/challenges";
|
import { generateChallenge } from "@/challenges";
|
||||||
import { randomString } from "@/math";
|
import { randomString } from "@/math";
|
||||||
import { Client as VersiaClient } from "@versia/client";
|
import { Client as VersiaClient } from "@versia/client";
|
||||||
|
|
@ -9,7 +10,6 @@ import { appFactory } from "~/app";
|
||||||
import { searchManager } from "~/classes/search/search-manager";
|
import { searchManager } from "~/classes/search/search-manager";
|
||||||
import { config } from "~/config.ts";
|
import { config } from "~/config.ts";
|
||||||
import { setupDatabase } from "~/drizzle/db";
|
import { setupDatabase } from "~/drizzle/db";
|
||||||
|
|
||||||
await setupDatabase();
|
await setupDatabase();
|
||||||
|
|
||||||
if (config.search.enabled) {
|
if (config.search.enabled) {
|
||||||
|
|
@ -210,3 +210,33 @@ export const getSolvedChallenge = async (): Promise<string> => {
|
||||||
}),
|
}),
|
||||||
).toString("base64");
|
).toString("base64");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mocks a module for the duration of a test
|
||||||
|
* Workaround for https://github.com/oven-sh/bun/issues/7823
|
||||||
|
*
|
||||||
|
* @param modulePath - The path starting from this files' path.
|
||||||
|
* @param renderMocks - Function to generate mocks (by their named or default exports)
|
||||||
|
* @returns An object with a dispose method
|
||||||
|
*/
|
||||||
|
export const mockModule = async (
|
||||||
|
modulePath: string,
|
||||||
|
renderMocks: () => Record<string, unknown>,
|
||||||
|
): Promise<{
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
}> => {
|
||||||
|
const original = {
|
||||||
|
...(await import(modulePath)),
|
||||||
|
};
|
||||||
|
const mocks = renderMocks();
|
||||||
|
const result = {
|
||||||
|
...original,
|
||||||
|
...mocks,
|
||||||
|
};
|
||||||
|
mock.module(modulePath, () => result);
|
||||||
|
return {
|
||||||
|
[Symbol.dispose]: (): void => {
|
||||||
|
mock.module(modulePath, () => original);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue