Skip to content

Commit

Permalink
chore: Setup and add e2e tests for the API endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
MohamedBassem committed Dec 30, 2024
1 parent 5aee340 commit 058e723
Show file tree
Hide file tree
Showing 15 changed files with 1,454 additions and 21 deletions.
12 changes: 12 additions & 0 deletions packages/e2e_tests/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
hoarder:
build:
dockerfile: docker/Dockerfile
context: ../../
target: aio
restart: unless-stopped
ports:
- "${HOARDER_PORT:-3000}:3000"
environment:
DATA_DIR: /tmp
NEXTAUTH_SECRET: secret
33 changes: 33 additions & 0 deletions packages/e2e_tests/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@hoarder/e2e_tests",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"format": "prettier . --ignore-path ../../.prettierignore",
"lint": "eslint .",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@hoarder/trpc": "workspace:^0.1.0",
"@hoarderapp/sdk": "workspace:^0.20.0",
"superjson": "^2.2.1"
},
"devDependencies": {
"@hoarder/eslint-config": "workspace:^0.2.0",
"@hoarder/prettier-config": "workspace:^0.1.0",
"@hoarder/tsconfig": "workspace:^0.1.0",
"vite-tsconfig-paths": "^4.3.1",
"vitest": "^1.3.1"
},
"eslintConfig": {
"root": true,
"extends": [
"@hoarder/eslint-config/base"
]
},
"prettier": "@hoarder/prettier-config"
}
27 changes: 27 additions & 0 deletions packages/e2e_tests/setup/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { GlobalSetupContext } from "vitest/node";

import { getTrpcClient } from "../utils/trpc";

export async function setup({ provide }: GlobalSetupContext) {
const trpc = getTrpcClient();
await trpc.users.create.mutate({
name: "Test User",
email: "[email protected]",
password: "test1234",
confirmPassword: "test1234",
});

const { key } = await trpc.apiKeys.exchange.mutate({
email: "[email protected]",
password: "test1234",
keyName: "test-key",
});
provide("adminApiKey", key);
return () => ({});
}

declare module "vitest" {
export interface ProvidedContext {
adminApiKey: string;
}
}
75 changes: 75 additions & 0 deletions packages/e2e_tests/setup/startContainers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { execSync } from "child_process";
import net from "net";
import path from "path";
import { fileURLToPath } from "url";
import type { GlobalSetupContext } from "vitest/node";

async function getRandomPort(): Promise<number> {
const server = net.createServer();
return new Promise<number>((resolve, reject) => {
server.unref();
server.on("error", reject);
server.listen(0, () => {
const port = (server.address() as net.AddressInfo).port;
server.close(() => resolve(port));
});
});
}

async function waitForHealthy(port: number, timeout = 60000): Promise<void> {
const startTime = Date.now();

while (Date.now() - startTime < timeout) {
try {
const response = await fetch(`http://localhost:${port}/api/health`);
if (response.status === 200) {
return;
}
} catch (error) {
// Ignore errors and retry
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}

throw new Error(`Health check failed after ${timeout}ms`);
}

export default async function ({ provide }: GlobalSetupContext) {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const port = await getRandomPort();

console.log(`Starting docker compose on port ${port}...`);
execSync(`docker compose up -d`, {
cwd: __dirname,
stdio: "inherit",
env: {
...process.env,
HOARDER_PORT: port.toString(),
},
});

console.log("Waiting for service to become healthy...");
await waitForHealthy(port);

// Wait 5 seconds for the worker to start
await new Promise((resolve) => setTimeout(resolve, 5000));

provide("hoarderPort", port);

process.env.HOARDER_PORT = port.toString();

return async () => {
console.log("Stopping docker compose...");
execSync("docker compose down", {
cwd: __dirname,
stdio: "inherit",
});
return Promise.resolve();
};
}

declare module "vitest" {
export interface ProvidedContext {
hoarderPort: number;
}
}
134 changes: 134 additions & 0 deletions packages/e2e_tests/tests/api/assets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { createHoarderClient } from "@hoarderapp/sdk";
import { assert, beforeEach, describe, expect, inject, it } from "vitest";

import { createTestUser, uploadTestAsset } from "../../utils/api";

describe("Assets API", () => {
const port = inject("hoarderPort");

if (!port) {
throw new Error("Missing required environment variables");
}

let client: ReturnType<typeof createHoarderClient>;
let apiKey: string;

beforeEach(async () => {
apiKey = await createTestUser();
client = createHoarderClient({

Check failure on line 18 in packages/e2e_tests/tests/api/assets.test.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe assignment of an `any` value

Check failure on line 18 in packages/e2e_tests/tests/api/assets.test.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe call of an `any` typed value
baseUrl: `http://localhost:${port}/api/v1/`,
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${apiKey}`,
},
});
});

it("should upload and retrieve an asset", async () => {
// Create a test file
const file = new File(["test content"], "test.pdf", {
type: "application/pdf",
});

// Upload the asset
const uploadResponse = await uploadTestAsset(apiKey, port, file);
expect(uploadResponse.assetId).toBeDefined();
expect(uploadResponse.contentType).toBe("application/pdf");
expect(uploadResponse.fileName).toBe("test.pdf");

// Retrieve the asset
const resp = await fetch(
`http://localhost:${port}/api/assets/${uploadResponse.assetId}`,
{
headers: {
authorization: `Bearer ${apiKey}`,
},
},
);

expect(resp.status).toBe(200);
});

it("should attach an asset to a bookmark", async () => {
// Create a test file
const file = new File(["test content"], "test.pdf", {
type: "application/pdf",
});

// Upload the asset
const uploadResponse = await uploadTestAsset(apiKey, port, file);

// Create a bookmark
const { data: createdBookmark } = await client.POST("/bookmarks", {

Check failure on line 62 in packages/e2e_tests/tests/api/assets.test.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe assignment of an `any` value

Check failure on line 62 in packages/e2e_tests/tests/api/assets.test.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe call of an `any` typed value

Check failure on line 62 in packages/e2e_tests/tests/api/assets.test.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe member access .POST on an `any` value
body: {
type: "asset",
title: "Test Asset Bookmark",
assetType: "pdf",
assetId: uploadResponse.assetId,
},
});

expect(createdBookmark).toBeDefined();
expect(createdBookmark?.id).toBeDefined();

Check failure on line 72 in packages/e2e_tests/tests/api/assets.test.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe member access .id on an `any` value

// Get the bookmark and verify asset
const { data: retrievedBookmark } = await client.GET(

Check failure on line 75 in packages/e2e_tests/tests/api/assets.test.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe assignment of an `any` value

Check failure on line 75 in packages/e2e_tests/tests/api/assets.test.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe call of an `any` typed value

Check failure on line 75 in packages/e2e_tests/tests/api/assets.test.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe member access .GET on an `any` value
"/bookmarks/{bookmarkId}",
{
params: {
path: {
bookmarkId: createdBookmark!.id,

Check failure on line 80 in packages/e2e_tests/tests/api/assets.test.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe assignment of an `any` value
},
},
},
);

expect(retrievedBookmark).toBeDefined();
assert(retrievedBookmark!.content.type === "asset");
expect(retrievedBookmark!.content.assetId).toBe(uploadResponse.assetId);
});

it("should delete asset when deleting bookmark", async () => {
// Create a test file
const file = new File(["test content"], "test.pdf", {
type: "application/pdf",
});

// Upload the asset
const uploadResponse = await uploadTestAsset(apiKey, port, file);

// Create a bookmark
const { data: createdBookmark } = await client.POST("/bookmarks", {
body: {
type: "asset",
title: "Test Asset Bookmark",
assetType: "pdf",
assetId: uploadResponse.assetId,
},
});

// Delete the bookmark
const { response: deleteResponse } = await client.DELETE(
"/bookmarks/{bookmarkId}",
{
params: {
path: {
bookmarkId: createdBookmark!.id,
},
},
},
);
expect(deleteResponse.status).toBe(204);

// Verify asset is deleted
const assetResponse = await fetch(
`http://localhost:${port}/api/assets/${uploadResponse.assetId}`,
{
headers: {
authorization: `Bearer ${apiKey}`,
},
},
);
expect(assetResponse.status).toBe(404);
});
});
Loading

0 comments on commit 058e723

Please sign in to comment.