From 403b1c7b87b5bc18cdd6839cb6d4c16765c29772 Mon Sep 17 00:00:00 2001
From: Till JS
Date: Sat, 21 Mar 2026 12:48:11 +0100
Subject: [PATCH] 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)
---
.../src/__tests__/utils/mock-factories.ts | 6 +-
.../backend/src/file/file.controller.spec.ts | 247 ++++++++++++++++++
.../src/folder/folder.controller.spec.ts | 150 +++++++++++
.../src/search/search.controller.spec.ts | 90 +++++++
.../src/share/share.controller.spec.ts | 161 ++++++++++++
.../backend/src/tag/tag.controller.spec.ts | 123 +++++++++
.../src/trash/trash.controller.spec.ts | 109 ++++++++
.../apps/web/src/routes/offline/+page.svelte | 31 +--
apps/storage/apps/web/vite.config.ts | 23 +-
docker/caddy/Caddyfile.production | 22 ++
10 files changed, 944 insertions(+), 18 deletions(-)
create mode 100644 apps/storage/apps/backend/src/file/file.controller.spec.ts
create mode 100644 apps/storage/apps/backend/src/folder/folder.controller.spec.ts
create mode 100644 apps/storage/apps/backend/src/search/search.controller.spec.ts
create mode 100644 apps/storage/apps/backend/src/share/share.controller.spec.ts
create mode 100644 apps/storage/apps/backend/src/tag/tag.controller.spec.ts
create mode 100644 apps/storage/apps/backend/src/trash/trash.controller.spec.ts
diff --git a/apps/storage/apps/backend/src/__tests__/utils/mock-factories.ts b/apps/storage/apps/backend/src/__tests__/utils/mock-factories.ts
index dbec53d87..40f7277d1 100644
--- a/apps/storage/apps/backend/src/__tests__/utils/mock-factories.ts
+++ b/apps/storage/apps/backend/src/__tests__/utils/mock-factories.ts
@@ -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,
diff --git a/apps/storage/apps/backend/src/file/file.controller.spec.ts b/apps/storage/apps/backend/src/file/file.controller.spec.ts
new file mode 100644
index 000000000..939966628
--- /dev/null
+++ b/apps/storage/apps/backend/src/file/file.controller.spec.ts
@@ -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;
+
+ 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);
+ 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();
+ });
+ });
+});
diff --git a/apps/storage/apps/backend/src/folder/folder.controller.spec.ts b/apps/storage/apps/backend/src/folder/folder.controller.spec.ts
new file mode 100644
index 000000000..48b675bf3
--- /dev/null
+++ b/apps/storage/apps/backend/src/folder/folder.controller.spec.ts
@@ -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;
+
+ 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);
+ 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);
+ });
+ });
+});
diff --git a/apps/storage/apps/backend/src/search/search.controller.spec.ts b/apps/storage/apps/backend/src/search/search.controller.spec.ts
new file mode 100644
index 000000000..81c27a8f6
--- /dev/null
+++ b/apps/storage/apps/backend/src/search/search.controller.spec.ts
@@ -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;
+
+ 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);
+ 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);
+ });
+ });
+});
diff --git a/apps/storage/apps/backend/src/share/share.controller.spec.ts b/apps/storage/apps/backend/src/share/share.controller.spec.ts
new file mode 100644
index 000000000..229970ac2
--- /dev/null
+++ b/apps/storage/apps/backend/src/share/share.controller.spec.ts
@@ -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;
+
+ 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);
+ 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 });
+ });
+ });
+});
diff --git a/apps/storage/apps/backend/src/tag/tag.controller.spec.ts b/apps/storage/apps/backend/src/tag/tag.controller.spec.ts
new file mode 100644
index 000000000..9f649b6df
--- /dev/null
+++ b/apps/storage/apps/backend/src/tag/tag.controller.spec.ts
@@ -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;
+
+ 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);
+ 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 });
+ });
+ });
+});
diff --git a/apps/storage/apps/backend/src/trash/trash.controller.spec.ts b/apps/storage/apps/backend/src/trash/trash.controller.spec.ts
new file mode 100644
index 000000000..6d782e023
--- /dev/null
+++ b/apps/storage/apps/backend/src/trash/trash.controller.spec.ts
@@ -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;
+
+ 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);
+ 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 });
+ });
+ });
+});
diff --git a/apps/storage/apps/web/src/routes/offline/+page.svelte b/apps/storage/apps/web/src/routes/offline/+page.svelte
index 987682508..d075bbc25 100644
--- a/apps/storage/apps/web/src/routes/offline/+page.svelte
+++ b/apps/storage/apps/web/src/routes/offline/+page.svelte
@@ -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}
{#if !isOnline}
-
-
- Zur Startseite
-
+