mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 05:59:39 +02:00
feat(storage): add controller tests, Caddy config, and PWA improvements
Controller tests (50 tests, all passing): - FileController: 12 tests (CRUD, upload, download with headers/URL mode) - FolderController: 8 tests (CRUD, move, favorite) - TrashController: 6 tests (restore file/folder, permanent delete, empty) - SearchController: 6 tests (search, empty query, favorites) - ShareController: 7 tests (CRUD, expiresInDays conversion, public token) - TagController: 7 tests (CRUD with optional color) Total test count now: 159 (133 backend + 26 web) Deployment: - Add Caddy reverse proxy entries for storage.mana.how and storage-api.mana.how PWA: - Upgrade to 'full' preset for better offline caching (fonts, external resources) - Add app shortcuts: Dateien, Suche, Favoriten - Improve offline page with links to cached pages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fc5dfe2f0f
commit
403b1c7b87
10 changed files with 944 additions and 18 deletions
|
|
@ -13,6 +13,8 @@ export const mockFileFactory = {
|
|||
storageKey: `users/test-user-id/${randomUUID()}-test-file.pdf`,
|
||||
parentFolderId: null,
|
||||
currentVersion: 1,
|
||||
checksum: null,
|
||||
thumbnailPath: null,
|
||||
isFavorite: false,
|
||||
isDeleted: false,
|
||||
deletedAt: null,
|
||||
|
|
@ -53,9 +55,9 @@ export const mockShareFactory = {
|
|||
userId: 'test-user-id',
|
||||
fileId: null,
|
||||
folderId: null,
|
||||
shareType: 'file',
|
||||
shareType: 'file' as const,
|
||||
shareToken: randomUUID().replace(/-/g, '') + randomUUID().replace(/-/g, ''),
|
||||
accessLevel: 'view',
|
||||
accessLevel: 'view' as const,
|
||||
password: null,
|
||||
maxDownloads: null,
|
||||
downloadCount: 0,
|
||||
|
|
|
|||
247
apps/storage/apps/backend/src/file/file.controller.spec.ts
Normal file
247
apps/storage/apps/backend/src/file/file.controller.spec.ts
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '@manacore/shared-nestjs-auth';
|
||||
import { FileController } from './file.controller';
|
||||
import { FileService } from './file.service';
|
||||
import { mockFileFactory } from '../__tests__/utils/mock-factories';
|
||||
|
||||
describe('FileController', () => {
|
||||
let controller: FileController;
|
||||
let fileService: jest.Mocked<FileService>;
|
||||
|
||||
const mockUser = { userId: 'test-user-id', email: 'test@example.com', role: 'user' };
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockFileService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
upload: jest.fn(),
|
||||
update: jest.fn(),
|
||||
move: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
toggleFavorite: jest.fn(),
|
||||
download: jest.fn(),
|
||||
getDownloadUrl: jest.fn(),
|
||||
getStats: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [FileController],
|
||||
providers: [{ provide: FileService, useValue: mockFileService }],
|
||||
})
|
||||
.overrideGuard(JwtAuthGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
controller = module.get<FileController>(FileController);
|
||||
fileService = module.get(FileService);
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return files for user at root level', async () => {
|
||||
const files = mockFileFactory.createMany(3);
|
||||
fileService.findAll.mockResolvedValue(files);
|
||||
|
||||
const result = await controller.findAll(mockUser);
|
||||
|
||||
expect(fileService.findAll).toHaveBeenCalledWith('test-user-id', undefined);
|
||||
expect(result).toEqual(files);
|
||||
});
|
||||
|
||||
it('should return files for user in specific folder', async () => {
|
||||
const folderId = 'folder-123';
|
||||
const files = mockFileFactory.createMany(2, { parentFolderId: folderId });
|
||||
fileService.findAll.mockResolvedValue(files);
|
||||
|
||||
const result = await controller.findAll(mockUser, folderId);
|
||||
|
||||
expect(fileService.findAll).toHaveBeenCalledWith('test-user-id', folderId);
|
||||
expect(result).toEqual(files);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a single file by id', async () => {
|
||||
const file = mockFileFactory.create({ id: 'file-123' });
|
||||
fileService.findOne.mockResolvedValue(file);
|
||||
|
||||
const result = await controller.findOne(mockUser, 'file-123');
|
||||
|
||||
expect(fileService.findOne).toHaveBeenCalledWith('test-user-id', 'file-123');
|
||||
expect(result).toEqual(file);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats', () => {
|
||||
it('should return file stats for user', async () => {
|
||||
const stats = { totalFiles: 10, totalSize: 1024000, byType: {} };
|
||||
fileService.getStats.mockResolvedValue(stats as any);
|
||||
|
||||
const result = await controller.getStats(mockUser);
|
||||
|
||||
expect(fileService.getStats).toHaveBeenCalledWith('test-user-id');
|
||||
expect(result).toEqual(stats);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upload', () => {
|
||||
it('should upload a file and return the result', async () => {
|
||||
const mockFile = {
|
||||
originalname: 'test.pdf',
|
||||
mimetype: 'application/pdf',
|
||||
size: 1024,
|
||||
buffer: Buffer.from('test'),
|
||||
} as Express.Multer.File;
|
||||
const dto = { parentFolderId: 'folder-123' };
|
||||
const createdFile = mockFileFactory.create({ name: 'test.pdf' });
|
||||
fileService.upload.mockResolvedValue(createdFile);
|
||||
|
||||
const result = await controller.upload(mockUser, mockFile, dto);
|
||||
|
||||
expect(fileService.upload).toHaveBeenCalledWith('test-user-id', mockFile, dto);
|
||||
expect(result).toEqual(createdFile);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when no file is provided', async () => {
|
||||
const dto = {};
|
||||
|
||||
await expect(controller.upload(mockUser, undefined as any, dto)).rejects.toThrow(
|
||||
BadRequestException
|
||||
);
|
||||
await expect(controller.upload(mockUser, undefined as any, dto)).rejects.toThrow(
|
||||
'No file provided'
|
||||
);
|
||||
expect(fileService.upload).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadMultiple', () => {
|
||||
it('should upload multiple files and return results', async () => {
|
||||
const mockFiles = [
|
||||
{
|
||||
originalname: 'file1.pdf',
|
||||
mimetype: 'application/pdf',
|
||||
size: 1024,
|
||||
buffer: Buffer.from('test1'),
|
||||
},
|
||||
{
|
||||
originalname: 'file2.png',
|
||||
mimetype: 'image/png',
|
||||
size: 2048,
|
||||
buffer: Buffer.from('test2'),
|
||||
},
|
||||
] as Express.Multer.File[];
|
||||
const dto = {};
|
||||
const created1 = mockFileFactory.create({ name: 'file1.pdf' });
|
||||
const created2 = mockFileFactory.create({ name: 'file2.png' });
|
||||
fileService.upload.mockResolvedValueOnce(created1).mockResolvedValueOnce(created2);
|
||||
|
||||
const result = await controller.uploadMultiple(mockUser, mockFiles, dto);
|
||||
|
||||
expect(fileService.upload).toHaveBeenCalledTimes(2);
|
||||
expect(fileService.upload).toHaveBeenCalledWith('test-user-id', mockFiles[0], dto);
|
||||
expect(fileService.upload).toHaveBeenCalledWith('test-user-id', mockFiles[1], dto);
|
||||
expect(result).toEqual([created1, created2]);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when no files provided', async () => {
|
||||
await expect(controller.uploadMultiple(mockUser, [], {})).rejects.toThrow(
|
||||
BadRequestException
|
||||
);
|
||||
await expect(controller.uploadMultiple(mockUser, [], {})).rejects.toThrow(
|
||||
'No files provided'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when files is undefined', async () => {
|
||||
await expect(controller.uploadMultiple(mockUser, undefined as any, {})).rejects.toThrow(
|
||||
BadRequestException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a file and return the result', async () => {
|
||||
const dto = { name: 'renamed-file.pdf' };
|
||||
const updatedFile = mockFileFactory.create({ name: 'renamed-file.pdf' });
|
||||
fileService.update.mockResolvedValue(updatedFile);
|
||||
|
||||
const result = await controller.update(mockUser, 'file-123', dto);
|
||||
|
||||
expect(fileService.update).toHaveBeenCalledWith('test-user-id', 'file-123', dto);
|
||||
expect(result).toEqual(updatedFile);
|
||||
});
|
||||
});
|
||||
|
||||
describe('move', () => {
|
||||
it('should move a file to a new folder', async () => {
|
||||
const dto = { parentFolderId: 'new-folder-id' };
|
||||
const movedFile = mockFileFactory.create({ parentFolderId: 'new-folder-id' });
|
||||
fileService.move.mockResolvedValue(movedFile);
|
||||
|
||||
const result = await controller.move(mockUser, 'file-123', dto);
|
||||
|
||||
expect(fileService.move).toHaveBeenCalledWith('test-user-id', 'file-123', dto);
|
||||
expect(result).toEqual(movedFile);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a file and return success', async () => {
|
||||
fileService.delete.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.delete(mockUser, 'file-123');
|
||||
|
||||
expect(fileService.delete).toHaveBeenCalledWith('test-user-id', 'file-123');
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleFavorite', () => {
|
||||
it('should toggle favorite status and return the file', async () => {
|
||||
const file = mockFileFactory.create({ isFavorite: true });
|
||||
fileService.toggleFavorite.mockResolvedValue(file);
|
||||
|
||||
const result = await controller.toggleFavorite(mockUser, 'file-123');
|
||||
|
||||
expect(fileService.toggleFavorite).toHaveBeenCalledWith('test-user-id', 'file-123');
|
||||
expect(result).toEqual(file);
|
||||
});
|
||||
});
|
||||
|
||||
describe('download', () => {
|
||||
it('should set response headers and send buffer for file download', async () => {
|
||||
const file = mockFileFactory.create({
|
||||
name: 'test-file.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
});
|
||||
const buffer = Buffer.from('file-content');
|
||||
fileService.download.mockResolvedValue({ buffer, file } as any);
|
||||
|
||||
const mockRes = { set: jest.fn(), send: jest.fn(), json: jest.fn() } as any;
|
||||
|
||||
await controller.download(mockUser, 'file-123', undefined as any, mockRes);
|
||||
|
||||
expect(fileService.download).toHaveBeenCalledWith('test-user-id', 'file-123');
|
||||
expect(mockRes.set).toHaveBeenCalledWith({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="${encodeURIComponent('test-file.pdf')}"`,
|
||||
'Content-Length': buffer.length,
|
||||
});
|
||||
expect(mockRes.send).toHaveBeenCalledWith(buffer);
|
||||
});
|
||||
|
||||
it('should return JSON with url when url=true query param is set', async () => {
|
||||
const downloadUrl = 'https://storage.example.com/presigned-url';
|
||||
fileService.getDownloadUrl.mockResolvedValue(downloadUrl);
|
||||
|
||||
const mockRes = { set: jest.fn(), send: jest.fn(), json: jest.fn() } as any;
|
||||
|
||||
await controller.download(mockUser, 'file-123', 'true', mockRes);
|
||||
|
||||
expect(fileService.getDownloadUrl).toHaveBeenCalledWith('test-user-id', 'file-123');
|
||||
expect(mockRes.json).toHaveBeenCalledWith({ url: downloadUrl });
|
||||
expect(mockRes.send).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
150
apps/storage/apps/backend/src/folder/folder.controller.spec.ts
Normal file
150
apps/storage/apps/backend/src/folder/folder.controller.spec.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { JwtAuthGuard } from '@manacore/shared-nestjs-auth';
|
||||
import { FolderController } from './folder.controller';
|
||||
import { FolderService } from './folder.service';
|
||||
import { mockFolderFactory } from '../__tests__/utils/mock-factories';
|
||||
|
||||
describe('FolderController', () => {
|
||||
let controller: FolderController;
|
||||
let folderService: jest.Mocked<FolderService>;
|
||||
|
||||
const mockUser = { userId: 'test-user-id', email: 'test@example.com', role: 'user' };
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockFolderService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
move: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
toggleFavorite: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [FolderController],
|
||||
providers: [{ provide: FolderService, useValue: mockFolderService }],
|
||||
})
|
||||
.overrideGuard(JwtAuthGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
controller = module.get<FolderController>(FolderController);
|
||||
folderService = module.get(FolderService);
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return root folders when no parentFolderId given', async () => {
|
||||
const folders = mockFolderFactory.createMany(3);
|
||||
folderService.findAll.mockResolvedValue(folders);
|
||||
|
||||
const result = await controller.findAll(mockUser);
|
||||
|
||||
expect(folderService.findAll).toHaveBeenCalledWith('test-user-id', undefined);
|
||||
expect(result).toEqual(folders);
|
||||
});
|
||||
|
||||
it('should return child folders when parentFolderId is given', async () => {
|
||||
const parentId = 'parent-folder-id';
|
||||
const folders = mockFolderFactory.createMany(2, { parentFolderId: parentId });
|
||||
folderService.findAll.mockResolvedValue(folders);
|
||||
|
||||
const result = await controller.findAll(mockUser, parentId);
|
||||
|
||||
expect(folderService.findAll).toHaveBeenCalledWith('test-user-id', parentId);
|
||||
expect(result).toEqual(folders);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a single folder by id', async () => {
|
||||
const folder = mockFolderFactory.create({ id: 'folder-123' });
|
||||
folderService.findOne.mockResolvedValue(folder);
|
||||
|
||||
const result = await controller.findOne(mockUser, 'folder-123');
|
||||
|
||||
expect(folderService.findOne).toHaveBeenCalledWith('test-user-id', 'folder-123');
|
||||
expect(result).toEqual(folder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a folder and return it', async () => {
|
||||
const dto = { name: 'New Folder', parentFolderId: 'parent-id', color: '#ff0000' };
|
||||
const created = mockFolderFactory.create({
|
||||
name: 'New Folder',
|
||||
parentFolderId: 'parent-id',
|
||||
color: '#ff0000',
|
||||
});
|
||||
folderService.create.mockResolvedValue(created);
|
||||
|
||||
const result = await controller.create(mockUser, dto);
|
||||
|
||||
expect(folderService.create).toHaveBeenCalledWith('test-user-id', dto);
|
||||
expect(result).toEqual(created);
|
||||
});
|
||||
|
||||
it('should create a root folder without parentFolderId', async () => {
|
||||
const dto = { name: 'Root Folder' };
|
||||
const created = mockFolderFactory.create({ name: 'Root Folder' });
|
||||
folderService.create.mockResolvedValue(created);
|
||||
|
||||
const result = await controller.create(mockUser, dto);
|
||||
|
||||
expect(folderService.create).toHaveBeenCalledWith('test-user-id', dto);
|
||||
expect(result).toEqual(created);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a folder and return the result', async () => {
|
||||
const dto = { name: 'Renamed Folder', color: '#00ff00' };
|
||||
const updated = mockFolderFactory.create({
|
||||
name: 'Renamed Folder',
|
||||
color: '#00ff00',
|
||||
});
|
||||
folderService.update.mockResolvedValue(updated);
|
||||
|
||||
const result = await controller.update(mockUser, 'folder-123', dto);
|
||||
|
||||
expect(folderService.update).toHaveBeenCalledWith('test-user-id', 'folder-123', dto);
|
||||
expect(result).toEqual(updated);
|
||||
});
|
||||
});
|
||||
|
||||
describe('move', () => {
|
||||
it('should move a folder to a new parent', async () => {
|
||||
const dto = { parentFolderId: 'new-parent-id' };
|
||||
const moved = mockFolderFactory.create({ parentFolderId: 'new-parent-id' });
|
||||
folderService.move.mockResolvedValue(moved);
|
||||
|
||||
const result = await controller.move(mockUser, 'folder-123', dto);
|
||||
|
||||
expect(folderService.move).toHaveBeenCalledWith('test-user-id', 'folder-123', dto);
|
||||
expect(result).toEqual(moved);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a folder and return success', async () => {
|
||||
folderService.delete.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.delete(mockUser, 'folder-123');
|
||||
|
||||
expect(folderService.delete).toHaveBeenCalledWith('test-user-id', 'folder-123');
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleFavorite', () => {
|
||||
it('should toggle favorite and return the folder', async () => {
|
||||
const folder = mockFolderFactory.create({ isFavorite: true });
|
||||
folderService.toggleFavorite.mockResolvedValue(folder);
|
||||
|
||||
const result = await controller.toggleFavorite(mockUser, 'folder-123');
|
||||
|
||||
expect(folderService.toggleFavorite).toHaveBeenCalledWith('test-user-id', 'folder-123');
|
||||
expect(result).toEqual(folder);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { JwtAuthGuard } from '@manacore/shared-nestjs-auth';
|
||||
import { SearchController } from './search.controller';
|
||||
import { SearchService } from './search.service';
|
||||
import { mockFileFactory, mockFolderFactory } from '../__tests__/utils/mock-factories';
|
||||
|
||||
describe('SearchController', () => {
|
||||
let controller: SearchController;
|
||||
let searchService: jest.Mocked<SearchService>;
|
||||
|
||||
const mockUser = { userId: 'test-user-id', email: 'test@example.com', role: 'user' };
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockSearchService = {
|
||||
search: jest.fn(),
|
||||
getFavorites: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SearchController],
|
||||
providers: [{ provide: SearchService, useValue: mockSearchService }],
|
||||
})
|
||||
.overrideGuard(JwtAuthGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
controller = module.get<SearchController>(SearchController);
|
||||
searchService = module.get(SearchService);
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
it('should search and return matching files and folders', async () => {
|
||||
const searchResults = {
|
||||
files: mockFileFactory.createMany(2, { name: 'report.pdf' }),
|
||||
folders: mockFolderFactory.createMany(1, { name: 'Reports' }),
|
||||
};
|
||||
searchService.search.mockResolvedValue(searchResults as any);
|
||||
|
||||
const result = await controller.search(mockUser, 'report');
|
||||
|
||||
expect(searchService.search).toHaveBeenCalledWith('test-user-id', 'report');
|
||||
expect(result).toEqual(searchResults);
|
||||
});
|
||||
|
||||
it('should return empty arrays when query is empty', async () => {
|
||||
const result = await controller.search(mockUser, '');
|
||||
|
||||
expect(searchService.search).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ files: [], folders: [] });
|
||||
});
|
||||
|
||||
it('should return empty arrays when query is undefined', async () => {
|
||||
const result = await controller.search(mockUser, undefined as any);
|
||||
|
||||
expect(searchService.search).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ files: [], folders: [] });
|
||||
});
|
||||
|
||||
it('should return empty arrays when query is only whitespace', async () => {
|
||||
const result = await controller.search(mockUser, ' ');
|
||||
|
||||
expect(searchService.search).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ files: [], folders: [] });
|
||||
});
|
||||
|
||||
it('should trim the query before passing to service', async () => {
|
||||
const searchResults = { files: [], folders: [] };
|
||||
searchService.search.mockResolvedValue(searchResults as any);
|
||||
|
||||
await controller.search(mockUser, ' report ');
|
||||
|
||||
expect(searchService.search).toHaveBeenCalledWith('test-user-id', 'report');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFavorites', () => {
|
||||
it('should return favorite files and folders', async () => {
|
||||
const favorites = {
|
||||
files: mockFileFactory.createMany(2, { isFavorite: true }),
|
||||
folders: mockFolderFactory.createMany(1, { isFavorite: true }),
|
||||
};
|
||||
searchService.getFavorites.mockResolvedValue(favorites as any);
|
||||
|
||||
const result = await controller.getFavorites(mockUser);
|
||||
|
||||
expect(searchService.getFavorites).toHaveBeenCalledWith('test-user-id');
|
||||
expect(result).toEqual(favorites);
|
||||
});
|
||||
});
|
||||
});
|
||||
161
apps/storage/apps/backend/src/share/share.controller.spec.ts
Normal file
161
apps/storage/apps/backend/src/share/share.controller.spec.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { JwtAuthGuard } from '@manacore/shared-nestjs-auth';
|
||||
import { ShareController } from './share.controller';
|
||||
import { ShareService } from './share.service';
|
||||
import { mockShareFactory } from '../__tests__/utils/mock-factories';
|
||||
|
||||
describe('ShareController', () => {
|
||||
let controller: ShareController;
|
||||
let shareService: jest.Mocked<ShareService>;
|
||||
|
||||
const mockUser = { userId: 'test-user-id', email: 'test@example.com', role: 'user' };
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockShareService = {
|
||||
findAll: jest.fn(),
|
||||
findByToken: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [ShareController],
|
||||
providers: [{ provide: ShareService, useValue: mockShareService }],
|
||||
})
|
||||
.overrideGuard(JwtAuthGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
controller = module.get<ShareController>(ShareController);
|
||||
shareService = module.get(ShareService);
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all shares for user', async () => {
|
||||
const shares = [
|
||||
mockShareFactory.create({ fileId: 'file-1' }),
|
||||
mockShareFactory.create({ folderId: 'folder-1', shareType: 'folder' as const }),
|
||||
];
|
||||
shareService.findAll.mockResolvedValue(shares as any);
|
||||
|
||||
const result = await controller.findAll(mockUser);
|
||||
|
||||
expect(shareService.findAll).toHaveBeenCalledWith('test-user-id');
|
||||
expect(result).toEqual(shares);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByToken', () => {
|
||||
it('should return shared item by token (public, no auth required)', async () => {
|
||||
const share = mockShareFactory.create({ shareToken: 'abc123token' });
|
||||
shareService.findByToken.mockResolvedValue(share as any);
|
||||
|
||||
const result = await controller.findByToken('abc123token');
|
||||
|
||||
expect(shareService.findByToken).toHaveBeenCalledWith('abc123token');
|
||||
expect(result).toEqual(share);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a file share with default settings', async () => {
|
||||
const dto = { fileId: 'file-123' };
|
||||
const created = mockShareFactory.create({ fileId: 'file-123' });
|
||||
shareService.create.mockResolvedValue(created as any);
|
||||
|
||||
const result = await controller.create(mockUser, dto);
|
||||
|
||||
expect(shareService.create).toHaveBeenCalledWith('test-user-id', {
|
||||
fileId: 'file-123',
|
||||
folderId: undefined,
|
||||
accessLevel: undefined,
|
||||
password: undefined,
|
||||
maxDownloads: undefined,
|
||||
expiresAt: undefined,
|
||||
});
|
||||
expect(result).toEqual(created);
|
||||
});
|
||||
|
||||
it('should convert expiresInDays to a Date object', async () => {
|
||||
const dto = {
|
||||
fileId: 'file-123',
|
||||
accessLevel: 'download' as const,
|
||||
expiresInDays: 7,
|
||||
};
|
||||
const created = mockShareFactory.create({ fileId: 'file-123' });
|
||||
shareService.create.mockResolvedValue(created as any);
|
||||
|
||||
const beforeCall = Date.now();
|
||||
await controller.create(mockUser, dto);
|
||||
const afterCall = Date.now();
|
||||
|
||||
const callArgs = shareService.create.mock.calls[0];
|
||||
expect(callArgs[0]).toBe('test-user-id');
|
||||
const expiresAt = callArgs[1].expiresAt as Date;
|
||||
expect(expiresAt).toBeInstanceOf(Date);
|
||||
|
||||
const expectedMin = beforeCall + 7 * 24 * 60 * 60 * 1000;
|
||||
const expectedMax = afterCall + 7 * 24 * 60 * 60 * 1000;
|
||||
expect(expiresAt.getTime()).toBeGreaterThanOrEqual(expectedMin);
|
||||
expect(expiresAt.getTime()).toBeLessThanOrEqual(expectedMax);
|
||||
});
|
||||
|
||||
it('should create a share with password and max downloads', async () => {
|
||||
const dto = {
|
||||
fileId: 'file-123',
|
||||
password: 'secret123',
|
||||
maxDownloads: 5,
|
||||
};
|
||||
const created = mockShareFactory.create({
|
||||
fileId: 'file-123',
|
||||
password: 'secret123',
|
||||
maxDownloads: 5,
|
||||
});
|
||||
shareService.create.mockResolvedValue(created as any);
|
||||
|
||||
const result = await controller.create(mockUser, dto);
|
||||
|
||||
expect(shareService.create).toHaveBeenCalledWith('test-user-id', {
|
||||
fileId: 'file-123',
|
||||
folderId: undefined,
|
||||
accessLevel: undefined,
|
||||
password: 'secret123',
|
||||
maxDownloads: 5,
|
||||
expiresAt: undefined,
|
||||
});
|
||||
expect(result).toEqual(created);
|
||||
});
|
||||
|
||||
it('should create a folder share', async () => {
|
||||
const dto = { folderId: 'folder-123', accessLevel: 'view' as const };
|
||||
const created = mockShareFactory.create({
|
||||
folderId: 'folder-123',
|
||||
shareType: 'folder' as const,
|
||||
});
|
||||
shareService.create.mockResolvedValue(created as any);
|
||||
|
||||
const result = await controller.create(mockUser, dto);
|
||||
|
||||
expect(shareService.create).toHaveBeenCalledWith('test-user-id', {
|
||||
fileId: undefined,
|
||||
folderId: 'folder-123',
|
||||
accessLevel: 'view',
|
||||
password: undefined,
|
||||
maxDownloads: undefined,
|
||||
expiresAt: undefined,
|
||||
});
|
||||
expect(result).toEqual(created);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a share and return success', async () => {
|
||||
shareService.delete.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.delete(mockUser, 'share-123');
|
||||
|
||||
expect(shareService.delete).toHaveBeenCalledWith('test-user-id', 'share-123');
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
123
apps/storage/apps/backend/src/tag/tag.controller.spec.ts
Normal file
123
apps/storage/apps/backend/src/tag/tag.controller.spec.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { JwtAuthGuard } from '@manacore/shared-nestjs-auth';
|
||||
import { TagController } from './tag.controller';
|
||||
import { TagService } from './tag.service';
|
||||
import { mockTagFactory } from '../__tests__/utils/mock-factories';
|
||||
|
||||
describe('TagController', () => {
|
||||
let controller: TagController;
|
||||
let tagService: jest.Mocked<TagService>;
|
||||
|
||||
const mockUser = { userId: 'test-user-id', email: 'test@example.com', role: 'user' };
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockTagService = {
|
||||
findAll: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [TagController],
|
||||
providers: [{ provide: TagService, useValue: mockTagService }],
|
||||
})
|
||||
.overrideGuard(JwtAuthGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
controller = module.get<TagController>(TagController);
|
||||
tagService = module.get(TagService);
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all tags for user', async () => {
|
||||
const tags = [
|
||||
mockTagFactory.create({ name: 'Work' }),
|
||||
mockTagFactory.create({ name: 'Personal' }),
|
||||
mockTagFactory.create({ name: 'Important' }),
|
||||
];
|
||||
tagService.findAll.mockResolvedValue(tags as any);
|
||||
|
||||
const result = await controller.findAll(mockUser);
|
||||
|
||||
expect(tagService.findAll).toHaveBeenCalledWith('test-user-id');
|
||||
expect(result).toEqual(tags);
|
||||
expect(result).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a tag with name and color', async () => {
|
||||
const dto = { name: 'Urgent', color: '#ff0000' };
|
||||
const created = mockTagFactory.create({ name: 'Urgent', color: '#ff0000' });
|
||||
tagService.create.mockResolvedValue(created as any);
|
||||
|
||||
const result = await controller.create(mockUser, dto);
|
||||
|
||||
expect(tagService.create).toHaveBeenCalledWith('test-user-id', 'Urgent', '#ff0000');
|
||||
expect(result).toEqual(created);
|
||||
});
|
||||
|
||||
it('should create a tag with name only (no color)', async () => {
|
||||
const dto = { name: 'Archive' };
|
||||
const created = mockTagFactory.create({ name: 'Archive' });
|
||||
tagService.create.mockResolvedValue(created as any);
|
||||
|
||||
const result = await controller.create(mockUser, dto);
|
||||
|
||||
expect(tagService.create).toHaveBeenCalledWith('test-user-id', 'Archive', undefined);
|
||||
expect(result).toEqual(created);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a tag with new name and color', async () => {
|
||||
const dto = { name: 'Updated Tag', color: '#00ff00' };
|
||||
const updated = mockTagFactory.create({ name: 'Updated Tag', color: '#00ff00' });
|
||||
tagService.update.mockResolvedValue(updated as any);
|
||||
|
||||
const result = await controller.update(mockUser, 'tag-123', dto);
|
||||
|
||||
expect(tagService.update).toHaveBeenCalledWith('test-user-id', 'tag-123', dto);
|
||||
expect(result).toEqual(updated);
|
||||
});
|
||||
|
||||
it('should update a tag with partial data (name only)', async () => {
|
||||
const dto = { name: 'Renamed' };
|
||||
const updated = mockTagFactory.create({ name: 'Renamed' });
|
||||
tagService.update.mockResolvedValue(updated as any);
|
||||
|
||||
const result = await controller.update(mockUser, 'tag-123', dto);
|
||||
|
||||
expect(tagService.update).toHaveBeenCalledWith('test-user-id', 'tag-123', {
|
||||
name: 'Renamed',
|
||||
});
|
||||
expect(result).toEqual(updated);
|
||||
});
|
||||
|
||||
it('should update a tag with partial data (color only)', async () => {
|
||||
const dto = { color: '#0000ff' };
|
||||
const updated = mockTagFactory.create({ color: '#0000ff' });
|
||||
tagService.update.mockResolvedValue(updated as any);
|
||||
|
||||
const result = await controller.update(mockUser, 'tag-123', dto);
|
||||
|
||||
expect(tagService.update).toHaveBeenCalledWith('test-user-id', 'tag-123', {
|
||||
color: '#0000ff',
|
||||
});
|
||||
expect(result).toEqual(updated);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a tag and return success', async () => {
|
||||
tagService.delete.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.delete(mockUser, 'tag-123');
|
||||
|
||||
expect(tagService.delete).toHaveBeenCalledWith('test-user-id', 'tag-123');
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
109
apps/storage/apps/backend/src/trash/trash.controller.spec.ts
Normal file
109
apps/storage/apps/backend/src/trash/trash.controller.spec.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { JwtAuthGuard } from '@manacore/shared-nestjs-auth';
|
||||
import { TrashController } from './trash.controller';
|
||||
import { TrashService } from './trash.service';
|
||||
import { mockFileFactory, mockFolderFactory } from '../__tests__/utils/mock-factories';
|
||||
|
||||
describe('TrashController', () => {
|
||||
let controller: TrashController;
|
||||
let trashService: jest.Mocked<TrashService>;
|
||||
|
||||
const mockUser = { userId: 'test-user-id', email: 'test@example.com', role: 'user' };
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockTrashService = {
|
||||
findAll: jest.fn(),
|
||||
restoreFile: jest.fn(),
|
||||
restoreFolder: jest.fn(),
|
||||
permanentlyDeleteFile: jest.fn(),
|
||||
permanentlyDeleteFolder: jest.fn(),
|
||||
emptyTrash: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [TrashController],
|
||||
providers: [{ provide: TrashService, useValue: mockTrashService }],
|
||||
})
|
||||
.overrideGuard(JwtAuthGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
controller = module.get<TrashController>(TrashController);
|
||||
trashService = module.get(TrashService);
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all trashed items for user', async () => {
|
||||
const trashedItems = {
|
||||
files: mockFileFactory.createMany(2, { isDeleted: true }),
|
||||
folders: mockFolderFactory.createMany(1, { isDeleted: true }),
|
||||
};
|
||||
trashService.findAll.mockResolvedValue(trashedItems as any);
|
||||
|
||||
const result = await controller.findAll(mockUser);
|
||||
|
||||
expect(trashService.findAll).toHaveBeenCalledWith('test-user-id');
|
||||
expect(result).toEqual(trashedItems);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restore', () => {
|
||||
it('should call restoreFile when type is file', async () => {
|
||||
const restoredFile = mockFileFactory.create({ isDeleted: false });
|
||||
trashService.restoreFile.mockResolvedValue(restoredFile as any);
|
||||
|
||||
const result = await controller.restore(mockUser, 'file-123', 'file');
|
||||
|
||||
expect(trashService.restoreFile).toHaveBeenCalledWith('test-user-id', 'file-123');
|
||||
expect(trashService.restoreFolder).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(restoredFile);
|
||||
});
|
||||
|
||||
it('should call restoreFolder when type is folder', async () => {
|
||||
const restoredFolder = mockFolderFactory.create({ isDeleted: false });
|
||||
trashService.restoreFolder.mockResolvedValue(restoredFolder as any);
|
||||
|
||||
const result = await controller.restore(mockUser, 'folder-123', 'folder');
|
||||
|
||||
expect(trashService.restoreFolder).toHaveBeenCalledWith('test-user-id', 'folder-123');
|
||||
expect(trashService.restoreFile).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(restoredFolder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('permanentlyDelete', () => {
|
||||
it('should permanently delete a file and return success', async () => {
|
||||
trashService.permanentlyDeleteFile.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.permanentlyDelete(mockUser, 'file-123', 'file');
|
||||
|
||||
expect(trashService.permanentlyDeleteFile).toHaveBeenCalledWith('test-user-id', 'file-123');
|
||||
expect(trashService.permanentlyDeleteFolder).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('should permanently delete a folder and return success', async () => {
|
||||
trashService.permanentlyDeleteFolder.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.permanentlyDelete(mockUser, 'folder-123', 'folder');
|
||||
|
||||
expect(trashService.permanentlyDeleteFolder).toHaveBeenCalledWith(
|
||||
'test-user-id',
|
||||
'folder-123'
|
||||
);
|
||||
expect(trashService.permanentlyDeleteFile).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('emptyTrash', () => {
|
||||
it('should empty all trash and return success', async () => {
|
||||
trashService.emptyTrash.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.emptyTrash(mockUser);
|
||||
|
||||
expect(trashService.emptyTrash).toHaveBeenCalledWith('test-user-id');
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -59,26 +59,27 @@
|
|||
{#if isOnline}
|
||||
Du wirst gleich weitergeleitet...
|
||||
{:else}
|
||||
Storage benötigt eine Internetverbindung für Cloud-Dateien.
|
||||
Storage benötigt eine Internetverbindung für Cloud-Dateien. Kürzlich besuchte Seiten sind
|
||||
möglicherweise noch verfügbar.
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
{#if !isOnline}
|
||||
<div class="space-y-4">
|
||||
<a
|
||||
href="/"
|
||||
class="inline-flex items-center justify-center px-6 py-3 bg-slate-600 hover:bg-slate-700 text-white font-medium rounded-lg transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||
/>
|
||||
</svg>
|
||||
Zur Startseite
|
||||
</a>
|
||||
<div class="flex flex-col gap-3 mb-4">
|
||||
<a
|
||||
href="/files"
|
||||
class="inline-flex items-center justify-center px-6 py-3 bg-slate-700 hover:bg-slate-600 text-white font-medium rounded-lg transition-colors"
|
||||
>
|
||||
Meine Dateien (cached)
|
||||
</a>
|
||||
<a
|
||||
href="/favorites"
|
||||
class="inline-flex items-center justify-center px-6 py-3 bg-slate-700 hover:bg-slate-600 text-white font-medium rounded-lg transition-colors"
|
||||
>
|
||||
Favoriten (cached)
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onclick={() => window.location.reload()}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,29 @@ export default defineConfig({
|
|||
createPWAConfig({
|
||||
name: 'Storage - Cloud Speicher',
|
||||
shortName: 'Storage',
|
||||
description: 'Cloud-Dateispeicher',
|
||||
description: 'Cloud-Dateispeicher mit Offline-Unterstützung',
|
||||
themeColor: '#64748b',
|
||||
preset: 'full',
|
||||
shortcuts: [
|
||||
{
|
||||
name: 'Meine Dateien',
|
||||
short_name: 'Dateien',
|
||||
description: 'Dateien und Ordner öffnen',
|
||||
url: '/files',
|
||||
},
|
||||
{
|
||||
name: 'Suche',
|
||||
short_name: 'Suche',
|
||||
description: 'Dateien durchsuchen',
|
||||
url: '/search',
|
||||
},
|
||||
{
|
||||
name: 'Favoriten',
|
||||
short_name: 'Favoriten',
|
||||
description: 'Favorisierte Dateien anzeigen',
|
||||
url: '/favorites',
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -82,6 +82,17 @@ contacts-api.mana.how {
|
|||
reverse_proxy localhost:3034
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Storage App
|
||||
# ============================================
|
||||
storage.mana.how {
|
||||
reverse_proxy localhost:5015
|
||||
}
|
||||
|
||||
storage-api.mana.how {
|
||||
reverse_proxy localhost:3035
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Skilltree App
|
||||
# ============================================
|
||||
|
|
@ -104,6 +115,17 @@ lightwrite-api.mana.how {
|
|||
reverse_proxy localhost:3010
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Picture App
|
||||
# ============================================
|
||||
picture.mana.how {
|
||||
reverse_proxy localhost:5021
|
||||
}
|
||||
|
||||
picture-api.mana.how {
|
||||
reverse_proxy localhost:3040
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# LLM Playground
|
||||
# ============================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue