papra/apps/papra-server/src/modules/documents/documents.repository.test.ts

257 lines
10 KiB
TypeScript

import { desc } from 'drizzle-orm';
import { describe, expect, test } from 'vitest';
import { createInMemoryDatabase } from '../app/database/database.test-utils';
import { ORGANIZATION_ROLES } from '../organizations/organizations.constants';
import { createDocumentAlreadyExistsError } from './documents.errors';
import { createDocumentsRepository } from './documents.repository';
import { documentsTable } from './documents.table';
describe('documents repository', () => {
describe('crud operations on document collection', () => {
test('a document can be created, retrieved, and soft deleted', async () => {
const { db } = await createInMemoryDatabase({
users: [{ id: 'user-1', email: 'user-1@example.com' }],
organizations: [{ id: 'organization-1', name: 'Organization 1' }],
organizationMembers: [{ organizationId: 'organization-1', userId: 'user-1', role: ORGANIZATION_ROLES.OWNER }],
});
const documentsRepository = createDocumentsRepository({ db });
const { document } = await documentsRepository.saveOrganizationDocument({
organizationId: 'organization-1',
createdBy: 'user-1',
mimeType: 'application/pdf',
name: 'Document 1',
originalName: 'document-1.pdf',
originalStorageKey: 'document-1.pdf',
originalSha256Hash: 'hash1',
});
expect(document).to.include({
organizationId: 'organization-1',
createdBy: 'user-1',
mimeType: 'application/pdf',
name: 'Document 1',
originalName: 'document-1.pdf',
originalStorageKey: 'document-1.pdf',
originalSha256Hash: 'hash1',
isDeleted: false,
});
const documents = await db.select().from(documentsTable).orderBy(desc(documentsTable.createdAt));
expect(documents).to.have.length(1);
expect(documents[0]).to.include({
organizationId: 'organization-1',
createdBy: 'user-1',
mimeType: 'application/pdf',
name: 'Document 1',
originalName: 'document-1.pdf',
originalStorageKey: 'document-1.pdf',
isDeleted: false,
});
await documentsRepository.softDeleteDocument({
documentId: document.id,
userId: 'user-1',
organizationId: 'organization-1',
});
const documentsAfterDelete = await db.select().from(documentsTable).orderBy(desc(documentsTable.createdAt));
expect(documentsAfterDelete).to.have.length(1);
expect(documentsAfterDelete[0]!.isDeleted).toBe(true);
});
});
describe('saveOrganizationDocument', () => {
test('a document is unique by organization, an error is raised if a document with the same hash already exists', async () => {
const { db } = await createInMemoryDatabase({
users: [{ id: 'user-1', email: 'user-1@example.com' }],
organizations: [{ id: 'organization-1', name: 'Organization 1' }],
});
const documentsRepository = createDocumentsRepository({ db });
await documentsRepository.saveOrganizationDocument({
organizationId: 'organization-1',
createdBy: 'user-1',
name: 'Document 1',
originalName: 'document-1.pdf',
content: 'lorem ipsum',
originalStorageKey: '',
mimeType: 'application/pdf',
originalSha256Hash: 'hash1',
});
await expect(
documentsRepository.saveOrganizationDocument({
organizationId: 'organization-1',
createdBy: 'user-1',
name: 'Document 1',
originalName: 'document-1.pdf',
content: 'lorem ipsum',
originalStorageKey: '',
mimeType: 'application/pdf',
originalSha256Hash: 'hash1',
}),
).rejects.toThrow(createDocumentAlreadyExistsError());
});
});
describe('getOrganizationStats', () => {
test('retrieve document count and total size for an organization', async () => {
const { db } = await createInMemoryDatabase({
users: [
{ id: 'user-1', email: 'user-1@example.com' },
{ id: 'user-2', email: 'user-2@example.com' },
],
organizations: [
{ id: 'organization-1', name: 'Organization 1' },
{ id: 'organization-2', name: 'Organization 2' },
],
organizationMembers: [
{ organizationId: 'organization-1', userId: 'user-1', role: ORGANIZATION_ROLES.OWNER },
{ organizationId: 'organization-2', userId: 'user-2', role: ORGANIZATION_ROLES.OWNER },
],
documents: [
{ id: 'doc-1', organizationId: 'organization-1', createdBy: 'user-1', name: 'Document 1', originalName: 'document-1.pdf', content: 'lorem ipsum', originalStorageKey: '', mimeType: 'application/pdf', originalSize: 200, originalSha256Hash: 'hash1' },
{ id: 'doc-2', organizationId: 'organization-1', createdBy: 'user-1', name: 'File 2', originalName: 'document-2.pdf', content: 'lorem', originalStorageKey: '', mimeType: 'application/pdf', originalSize: 10, originalSha256Hash: 'hash2' },
{ id: 'doc-3', organizationId: 'organization-1', createdBy: 'user-1', name: 'File 3', originalName: 'document-3.pdf', content: 'ipsum', originalStorageKey: '', mimeType: 'application/pdf', originalSize: 5, originalSha256Hash: 'hash3' },
{ id: 'doc-4', organizationId: 'organization-2', createdBy: 'user-2', name: 'File 3', originalName: 'document-3.pdf', content: 'ipsum', originalStorageKey: '', mimeType: 'application/pdf', originalSize: 100, originalSha256Hash: 'hash4' },
{ id: 'doc-5', organizationId: 'organization-1', createdBy: 'user-2', name: 'File 3', originalName: 'document-3.pdf', content: 'ipsum', originalStorageKey: '', mimeType: 'application/pdf', originalSize: 100, originalSha256Hash: 'hash5', deletedAt: new Date(0), isDeleted: true },
{ id: 'doc-6', organizationId: 'organization-1', createdBy: 'user-2', name: 'File 3', originalName: 'document-3.pdf', content: 'ipsum', originalStorageKey: '', mimeType: 'application/pdf', originalSize: 100, originalSha256Hash: 'hash6', deletedAt: new Date(0), isDeleted: true },
],
});
const documentsRepository = createDocumentsRepository({ db });
const stats = await documentsRepository.getOrganizationStats({
organizationId: 'organization-1',
});
expect(stats).to.deep.equal({
documentsCount: 3,
documentsSize: 215,
totalDocumentsSize: 415,
totalDocumentsCount: 5,
deletedDocumentsCount: 2,
deletedDocumentsSize: 200,
});
});
test('returns 0 count and size when no documents are present', async () => {
const { db } = await createInMemoryDatabase({
users: [
{ id: 'user-1', email: 'user-1@example.com' },
],
organizations: [
{ id: 'organization-1', name: 'Organization 1' },
],
organizationMembers: [
{ organizationId: 'organization-1', userId: 'user-1', role: ORGANIZATION_ROLES.OWNER },
],
});
const documentsRepository = createDocumentsRepository({ db });
const stats = await documentsRepository.getOrganizationStats({
organizationId: 'organization-1',
});
expect(stats).to.deep.equal({
documentsCount: 0,
documentsSize: 0,
totalDocumentsSize: 0,
totalDocumentsCount: 0,
deletedDocumentsCount: 0,
deletedDocumentsSize: 0,
});
});
test('returns 0 count and size when organization does not exist', async () => {
const { db } = await createInMemoryDatabase();
const documentsRepository = createDocumentsRepository({ db });
const stats = await documentsRepository.getOrganizationStats({
organizationId: 'organization-1',
});
expect(stats).to.deep.equal({
documentsCount: 0,
documentsSize: 0,
totalDocumentsSize: 0,
totalDocumentsCount: 0,
deletedDocumentsCount: 0,
deletedDocumentsSize: 0,
});
});
});
describe('areAllDocumentsInOrganization', async () => {
const { db } = await createInMemoryDatabase({
organizations: [
{ id: 'org-1', name: 'Organization 1' },
{ id: 'org-2', name: 'Organization 2' },
],
documents: [
{ id: 'doc-1', organizationId: 'org-1', mimeType: 'text/plain', originalStorageKey: 'org-1/originals/doc-1.txt', name: 'file.txt', originalName: 'file.txt', originalSha256Hash: 'hash1' },
{ id: 'doc-2', organizationId: 'org-1', mimeType: 'text/plain', originalStorageKey: 'org-1/originals/doc-2.txt', name: 'file.txt', originalName: 'file.txt', originalSha256Hash: 'hash2' },
{ id: 'doc-3', organizationId: 'org-2', mimeType: 'text/plain', originalStorageKey: 'org-2/originals/doc-3.txt', name: 'file.txt', originalName: 'file.txt', originalSha256Hash: 'hash3' },
],
});
const documentsRepository = createDocumentsRepository({ db });
test('check if a given list of document IDs all belong to the organization', async () => {
expect(
await documentsRepository.areAllDocumentsInOrganization({
documentIds: ['doc-1', 'doc-2'],
organizationId: 'org-1',
}),
).to.eql(true);
expect(
await documentsRepository.areAllDocumentsInOrganization({
documentIds: ['doc-1', 'doc-3'],
organizationId: 'org-1',
}),
).to.eql(false);
expect(
await documentsRepository.areAllDocumentsInOrganization({
documentIds: ['doc-3'],
organizationId: 'org-1',
}),
).to.eql(false);
});
test('no document IDs provided, returns true', async () => {
expect(
await documentsRepository.areAllDocumentsInOrganization({
documentIds: [],
organizationId: 'org-1',
}),
).to.eql(true);
});
test('non existing document ID returns false', async () => {
expect(
await documentsRepository.areAllDocumentsInOrganization({
documentIds: ['non-existing-doc'],
organizationId: 'org-1',
}),
).to.eql(false);
});
test('duplicated document IDs from the organizations are accepted and return true', async () => {
expect(
await documentsRepository.areAllDocumentsInOrganization({
documentIds: ['doc-1', 'doc-1', 'doc-2'],
organizationId: 'org-1',
}),
).to.eql(true);
});
});
});