paperless-gpt/web-app/e2e/test-environment.ts

279 lines
8.8 KiB
TypeScript

import { Browser, chromium } from '@playwright/test';
import * as fs from 'fs';
import { GenericContainer, Network, StartedTestContainer, Wait } from 'testcontainers';
export interface TestEnvironment {
paperlessNgx: StartedTestContainer;
paperlessGpt: StartedTestContainer;
browser: Browser;
cleanup: () => Promise<void>;
}
export const PORTS = {
paperlessNgx: 8000,
paperlessGpt: 8080,
};
export async function setupTestEnvironment(): Promise<TestEnvironment> {
console.log('Setting up test environment...');
const paperlessPort = PORTS.paperlessNgx;
const gptPort = PORTS.paperlessGpt;
// Create a network for the containers
const network = await new Network().start();
console.log('Starting Redis container...');
const redis = await new GenericContainer('redis:7')
.withNetwork(network)
.withNetworkAliases('redis')
.start();
console.log('Starting Postgres container...');
const postgres = await new GenericContainer('postgres:15')
.withNetwork(network)
.withNetworkAliases('postgres')
.withEnvironment({
POSTGRES_DB: 'paperless',
POSTGRES_USER: 'paperless',
POSTGRES_PASSWORD: 'paperless'
})
.start();
console.log('Starting Paperless-ngx container...');
const paperlessNgx = await new GenericContainer('ghcr.io/paperless-ngx/paperless-ngx:latest')
.withNetwork(network)
.withNetworkAliases('paperless-ngx')
.withEnvironment({
PAPERLESS_URL: `http://localhost:${paperlessPort}`,
PAPERLESS_SECRET_KEY: 'change-me',
PAPERLESS_ADMIN_USER: 'admin',
PAPERLESS_ADMIN_PASSWORD: 'admin',
PAPERLESS_TIME_ZONE: 'Europe/Berlin',
PAPERLESS_OCR_LANGUAGE: 'eng',
PAPERLESS_REDIS: 'redis://redis:6379',
PAPERLESS_DBHOST: 'postgres',
PAPERLESS_DBNAME: 'paperless',
PAPERLESS_DBUSER: 'paperless',
PAPERLESS_DBPASS: 'paperless'
})
.withExposedPorts(paperlessPort)
.withWaitStrategy(Wait.forHttp('/api/', paperlessPort))
.start();
const mappedPort = paperlessNgx.getMappedPort(paperlessPort);
console.log(`Paperless-ngx container started, mapped port: ${mappedPort}`);
// Create required tag before starting paperless-gpt
const baseUrl = `http://localhost:${mappedPort}`;
const credentials = { username: 'admin', password: 'admin' };
try {
console.log('Creating paperless-gpt tag...');
await createTag(baseUrl, 'paperless-gpt', credentials);
} catch (error) {
console.error('Failed to create paperless-gpt tag:', error);
await paperlessNgx.stop();
throw error;
}
console.log('Starting Paperless-gpt container...');
const paperlessGpt = await new GenericContainer('icereed/paperless-gpt:e2e')
.withNetwork(network)
.withEnvironment({
PAPERLESS_BASE_URL: `http://paperless-ngx:${paperlessPort}`,
PAPERLESS_API_TOKEN: await getApiToken(baseUrl, credentials),
LLM_PROVIDER: "openai",
LLM_MODEL: "gpt-4",
LLM_LANGUAGE: "english",
OPENAI_API_KEY: process.env.OPENAI_API_KEY || '',
})
.withExposedPorts(gptPort)
.withWaitStrategy(Wait.forHttp('/', gptPort))
.start();
console.log('Paperless-gpt container started');
console.log('Launching browser...');
const browser = await chromium.launch();
console.log('Browser launched');
const cleanup = async () => {
console.log('Cleaning up test environment...');
await browser.close();
await paperlessGpt.stop();
await paperlessNgx.stop();
await redis.stop();
await postgres.stop();
await network.stop();
console.log('Test environment cleanup completed');
};
console.log('Test environment setup completed');
return {
paperlessNgx,
paperlessGpt,
browser,
cleanup,
};
}
export async function waitForElement(page: any, selector: string, timeout = 5000): Promise<void> {
await page.waitForSelector(selector, { timeout });
}
export interface PaperlessDocument {
id: number;
title: string;
content: string;
tags: number[];
}
// Helper to upload a document via Paperless-ngx API
export async function uploadDocument(
baseUrl: string,
filePath: string,
title: string,
credentials: { username: string; password: string }
): Promise<PaperlessDocument> {
console.log(`Uploading document: ${title} from ${filePath}`);
const formData = new FormData();
const fileData = await fs.promises.readFile(filePath);
formData.append('document', new Blob([fileData]));
formData.append('title', title);
// Initial upload to get task ID
const uploadResponse = await fetch(`${baseUrl}/api/documents/post_document/`, {
method: 'POST',
body: formData,
headers: {
'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`),
},
});
if (!uploadResponse.ok) {
console.error(`Upload failed with status ${uploadResponse.status}: ${uploadResponse.statusText}`);
throw new Error(`Failed to upload document: ${uploadResponse.statusText}`);
}
const task_id = await uploadResponse.json();
// Poll the tasks endpoint until document is processed
while (true) {
console.log(`Checking task status for ID: ${task_id}`);
const taskResponse = await fetch(`${baseUrl}/api/tasks/?task_id=${task_id}`, {
headers: {
'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`),
},
});
if (!taskResponse.ok) {
throw new Error(`Failed to check task status: ${taskResponse.statusText}`);
}
const taskResultArr = await taskResponse.json();
console.log(`Task status: ${JSON.stringify(taskResultArr)}`);
if (taskResultArr.length === 0) {
continue;
}
const taskResult = taskResultArr[0];
// Check if task is completed
if (taskResult.status === 'SUCCESS' && taskResult.id) {
console.log(`Document processed successfully with ID: ${taskResult.id}`);
// Fetch the complete document details
const documentResponse = await fetch(`${baseUrl}/api/documents/${taskResult.id}/`, {
headers: {
'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`),
},
});
if (!documentResponse.ok) {
throw new Error(`Failed to fetch document details: ${documentResponse.statusText}`);
}
return await documentResponse.json();
}
// Check for failure
if (taskResult.status === 'FAILED') {
throw new Error(`Document processing failed: ${taskResult.result}`);
}
// Wait before polling again
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
// Helper to create a tag via Paperless-ngx API
export async function createTag(
baseUrl: string,
name: string,
credentials: { username: string; password: string }
): Promise<number> {
console.log(`Creating tag: ${name}`);
const response = await fetch(`${baseUrl}/api/tags/`, {
method: 'POST',
body: JSON.stringify({ name }),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`),
},
});
if (!response.ok) {
console.error(`Tag creation failed with status ${response.status}: ${response.statusText}`);
throw new Error(`Failed to create tag: ${response.statusText}`);
}
const tag = await response.json();
console.log(`Tag created successfully with ID: ${tag.id}`);
return tag.id;
}
// Helper to get an API token
export async function getApiToken(
baseUrl: string,
credentials: { username: string; password: string }
): Promise<string> {
console.log('Fetching API token');
const response = await fetch(`${baseUrl}/api/token/`, {
method: 'POST',
body: new URLSearchParams({
username: credentials.username,
password: credentials.password,
}),
});
if (!response.ok) {
console.error(`API token fetch failed with status ${response.status}: ${response.statusText}`);
throw new Error(`Failed to fetch API token: ${response.statusText}`);
}
const token = await response.json();
console.log(`API token fetched successfully: ${token.token}`);
return token.token;
}
// Helper to add a tag to a document
export async function addTagToDocument(
baseUrl: string,
documentId: number,
tagId: number,
credentials: { username: string; password: string }
): Promise<void> {
console.log(`Adding tag ${tagId} to document ${documentId}`);
const response = await fetch(`${baseUrl}/api/documents/${documentId}/`, {
method: 'PATCH',
body: JSON.stringify({
tags: [tagId],
}),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`),
},
});
if (!response.ok) {
console.error(`Tag addition failed with status ${response.status}: ${response.statusText}`);
throw new Error(`Failed to add tag to document: ${response.statusText}`);
}
console.log('Tag added successfully');
}