-
-
Notifications
You must be signed in to change notification settings - Fork 478
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: Setup and add e2e tests for the API endpoints
- Loading branch information
1 parent
5aee340
commit 058e723
Showing
15 changed files
with
1,454 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
|
||
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
|
||
body: { | ||
type: "asset", | ||
title: "Test Asset Bookmark", | ||
assetType: "pdf", | ||
assetId: uploadResponse.assetId, | ||
}, | ||
}); | ||
|
||
expect(createdBookmark).toBeDefined(); | ||
expect(createdBookmark?.id).toBeDefined(); | ||
|
||
// 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
|
||
"/bookmarks/{bookmarkId}", | ||
{ | ||
params: { | ||
path: { | ||
bookmarkId: createdBookmark!.id, | ||
}, | ||
}, | ||
}, | ||
); | ||
|
||
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); | ||
}); | ||
}); |
Oops, something went wrong.