21 Commits

Author SHA1 Message Date
8fbc59499b fix fake URL in test fixtures pointing to 404-page and refactoring magic strings URLs 2025-09-30 10:40:29 +02:00
66b0d1cb3c refactoring magic strings URL in Anmeldung view 2025-09-30 09:34:01 +02:00
45a5c46489 refactoring magic strings: in Vorgang view load() function 2025-09-30 09:09:02 +02:00
c93a5c50de refactoring magic strings: in Vorgang View, add CRIME API route 2025-09-30 09:07:04 +02:00
2ad5a67340 refactoring magic strings: Anmeldung URL with Vorgang in view guard 2025-09-30 08:31:33 +02:00
e1694552c9 refactoring magic strings: User API URLs in user-management view 2025-09-30 08:22:56 +02:00
50a9286895 refactoring magic strings API URLs in upload view 2025-09-30 08:16:28 +02:00
e79d0a1a2d refactoring magic urls:. anmeldung actions 2025-09-29 08:49:26 +02:00
1d2769d114 refactoring magic strings in upload actions 2025-09-29 08:42:54 +02:00
387a9b21a8 refactoring in auth-service magic urls: path 2025-09-29 08:07:04 +02:00
7d0ec1283b refactoring hooks.server.ts magic urls: path 2025-09-29 08:00:00 +02:00
0e13744a79 refactor test 2025-09-26 12:47:55 +02:00
f737e4da4c refactoring: magic strings in Vorgang-View 2025-09-26 12:34:15 +02:00
f43497d69c refactoring magic strings in layout.server file (guard), including new routes and tests 2025-09-26 11:59:21 +02:00
59abf0880d add conditional route and crimeList View refactoring + tests 2025-09-26 10:04:24 +02:00
ca88a541c8 refactoring: magic strings and tests for Vorgang overview page 2025-09-26 08:32:42 +02:00
0820622ace make param in routes optional 2025-09-26 08:31:21 +02:00
67a24f3650 test links on Home-Page View 2025-09-25 14:05:38 +02:00
f0b133101d remove option 2025-09-25 13:53:59 +02:00
7396e15241 use userData from fixtures in Footer and Header 2025-09-25 13:44:50 +02:00
e1ce9373c0 refactor magic links in Header file, incl. routes 2025-09-25 13:29:07 +02:00
22 changed files with 205 additions and 51 deletions

View File

@@ -1,5 +1,7 @@
import { decryptToken } from '$lib/auth'; import { decryptToken } from '$lib/auth';
import type { Handle } from '@sveltejs/kit'; import type { Handle } from '@sveltejs/kit';
import { ROUTE_NAMES } from './routes';
export const handle: Handle = async ({ event, resolve }) => { export const handle: Handle = async ({ event, resolve }) => {
const jwt = event.cookies.get('session'); const jwt = event.cookies.get('session');
@@ -9,7 +11,7 @@ export const handle: Handle = async ({ event, resolve }) => {
return resolve(event); return resolve(event);
} }
} catch (_) { } catch (_) {
event.cookies.delete('session', {path: '/'}); event.cookies.delete('session', {path: ROUTE_NAMES.ROOT});
event.locals.user = null; event.locals.user = null;
} }
return await resolve(event); return await resolve(event);

View File

@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import Chevron from '$lib/icons/Chevron-right.svelte'; import Chevron from '$lib/icons/Chevron-right.svelte';
import { ROUTE_NAMES } from '../../routes';
export let data; export let data;
</script> </script>
@@ -11,7 +13,7 @@
aria-label="Global" aria-label="Global"
> >
<div class="flex w-48"> <div class="flex w-48">
<a href="/" class="-m-1.5 p-1.5 w-10"> <a href="{ROUTE_NAMES.ROOT}" class="-m-1.5 p-1.5 w-10">
<span class="sr-only">Tatort Niedersachen</span> <span class="sr-only">Tatort Niedersachen</span>
<img class="h-8 w-auto" src="/Landeswappen_NI.svg" alt="Landeswappen Niedersachsen" /> <img class="h-8 w-auto" src="/Landeswappen_NI.svg" alt="Landeswappen Niedersachsen" />
</a> </a>
@@ -19,7 +21,7 @@
<h1 class="text-3xl text-slate-400 font-bold">Tatort</h1> <h1 class="text-3xl text-slate-400 font-bold">Tatort</h1>
<div class="lg:flex lg:justify-end w-48"> <div class="lg:flex lg:justify-end w-48">
{#if data.user} {#if data.user}
<form method="POST" action="/anmeldung?/logout"> <form method="POST" action="{ROUTE_NAMES.ANMELDUNG_LOGOUT}">
<input type="hidden" /> <input type="hidden" />
<button type="submit" class="text-sm font-semibold leading-6 text-gray-900" <button type="submit" class="text-sm font-semibold leading-6 text-gray-900"
><span ><span

View File

@@ -1,6 +1,7 @@
import { dev } from '$app/environment'; import { dev } from '$app/environment';
import { fail, redirect, type Cookies, type RequestEvent } from '@sveltejs/kit'; import { fail, redirect, type Cookies, type RequestEvent } from '@sveltejs/kit';
import { authenticate } from '$lib/auth'; import { authenticate } from '$lib/auth';
import { ROUTE_NAMES } from '../../routes';
const COOKIE_NAME = 'session'; const COOKIE_NAME = 'session';
@@ -14,16 +15,16 @@ export const loginUser = async ({ request, cookies }: { request: Request; cookie
if (!token) return fail(400, { user, incorrect: true }); if (!token) return fail(400, { user, incorrect: true });
cookies.set(COOKIE_NAME, token, { cookies.set(COOKIE_NAME, token, {
path: '/', path: ROUTE_NAMES.ROOT,
httpOnly: true, httpOnly: true,
sameSite: 'strict', sameSite: 'strict',
secure: !dev secure: !dev
}); });
return redirect(303, '/'); return redirect(303, ROUTE_NAMES.ROOT);
}; };
export const logoutUser = async (event: RequestEvent) => { export const logoutUser = async (event: RequestEvent) => {
event.cookies.delete(COOKIE_NAME, { path: '/' }); event.cookies.delete(COOKIE_NAME, { path: ROUTE_NAMES.ROOT });
event.locals.user = null; event.locals.user = null;
return { success: true }; return { success: true };
}; };

View File

@@ -1,10 +1,12 @@
import { redirect, type ServerLoadEvent } from '@sveltejs/kit'; import { redirect, type ServerLoadEvent } from '@sveltejs/kit';
import type { PageServerLoad } from '../anmeldung/$types'; import type { PageServerLoad } from '../anmeldung/$types';
import { ROUTE_NAMES } from '..';
export const load: PageServerLoad = (event: ServerLoadEvent) => { export const load: PageServerLoad = (event: ServerLoadEvent) => {
if (!event.locals.user && event.url.pathname !== '/anmeldung') throw redirect(303, '/anmeldung'); if (!event.locals.user && event.url.pathname !== ROUTE_NAMES.ANMELDUNG)
throw redirect(303, ROUTE_NAMES.ANMELDUNG);
return { return {
user: event.locals.user user: event.locals.user
}; };
} };

View File

@@ -3,6 +3,8 @@
import FileRect from '$lib/icons/File-rect.svelte'; import FileRect from '$lib/icons/File-rect.svelte';
import ListIcon from '$lib/icons/List-icon.svelte'; import ListIcon from '$lib/icons/List-icon.svelte';
import { ROUTE_NAMES } from '../index.js';
export let data; export let data;
export let outline = true; export let outline = true;
</script> </script>
@@ -18,7 +20,7 @@
> >
<ListIcon class=" group-hover:text-indigo-600" /> <ListIcon class=" group-hover:text-indigo-600" />
</div> </div>
<a href="/list" class="mt-6 block font-semibold text-gray-900"> <a href="{ROUTE_NAMES.LIST}" class="mt-6 block font-semibold text-gray-900">
Vorgänge Vorgänge
<span class="absolute inset-0"></span> <span class="absolute inset-0"></span>
</a> </a>
@@ -34,7 +36,7 @@
> >
<AddProcess class=" group-hover:text-indigo-600" /> <AddProcess class=" group-hover:text-indigo-600" />
</div> </div>
<a href="/upload" class="mt-6 block font-semibold text-gray-900"> <a href="{ROUTE_NAMES.UPLOAD}" class="mt-6 block font-semibold text-gray-900">
Hinzufügen Hinzufügen
<span class="absolute inset-0"></span> <span class="absolute inset-0"></span>
</a> </a>
@@ -47,7 +49,7 @@
> >
<FileRect class=" group-hover:text-indigo-600" {outline} /> <FileRect class=" group-hover:text-indigo-600" {outline} />
</div> </div>
<a href="/user-management" class="mt-6 block font-semibold text-gray-900"> <a href="{ROUTE_NAMES.USERMGMT}" class="mt-6 block font-semibold text-gray-900">
Benutzerverwaltung Benutzerverwaltung
<span class="absolute inset-0"></span> <span class="absolute inset-0"></span>
</a> </a>

View File

@@ -2,6 +2,7 @@
import Trash from '$lib/icons/Trash.svelte'; import Trash from '$lib/icons/Trash.svelte';
import Folder from '$lib/icons/Folder.svelte'; import Folder from '$lib/icons/Folder.svelte';
import EmptyList from '$lib/components/EmptyList.svelte'; import EmptyList from '$lib/components/EmptyList.svelte';
import { API_ROUTES, ROUTE_NAMES } from '../../index.js';
let { data } = $props(); let { data } = $props();
@@ -17,7 +18,7 @@
if (!target) return; if (!target) return;
let filename = target.id.split('del__')[1]; let filename = target.id.split('del__')[1];
let url = `/api/list/${filename}`; let url = API_ROUTES.VORGANG(filename);
try { try {
const response = await fetch(url, { method: 'DELETE' }); const response = await fetch(url, { method: 'DELETE' });
@@ -49,7 +50,7 @@
{#each vorgangList as vorgangItem} {#each vorgangList as vorgangItem}
<li data-testid="test-list-item"> <li data-testid="test-list-item">
<a <a
href="/list/{vorgangItem.vorgangToken}?pin={vorgangItem.vorgangPIN}" href="{ROUTE_NAMES.VORGANG(vorgangItem.vorgangToken, vorgangItem.vorgangPIN)}"
class="flex justify-between gap-x-6 py-5" class="flex justify-between gap-x-6 py-5"
> >
<div class="flex gap-x-4"> <div class="flex gap-x-4">

View File

@@ -9,6 +9,7 @@
import shortenFileSize from '$lib/helper/shortenFileSize.js'; import shortenFileSize from '$lib/helper/shortenFileSize.js';
import Exclamation from '$lib/icons/Exclamation.svelte'; import Exclamation from '$lib/icons/Exclamation.svelte';
import FileRect from '$lib/icons/File-rect.svelte'; import FileRect from '$lib/icons/File-rect.svelte';
import { API_ROUTES, ROUTE_NAMES } from '../../index.js';
export let form; export let form;
@@ -43,7 +44,7 @@
data.append('vorgang', vorgang); data.append('vorgang', vorgang);
data.append('name', name); data.append('name', name);
data.append('vorgangPIN', vorgangPIN); data.append('vorgangPIN', vorgangPIN);
const response = await fetch('?/validate', { method: 'POST', body: data }); const response = await fetch(ROUTE_NAMES.UPLOAD_VALIDATE, { method: 'POST', body: data });
/** @type {import('@sveltejs/kit').ActionResult} */ /** @type {import('@sveltejs/kit').ActionResult} */
const result = deserialize(await response.text()); const result = deserialize(await response.text());
@@ -76,7 +77,7 @@
data.append('type', files[0].type); data.append('type', files[0].type);
data.append('fileName', files[0].name); data.append('fileName', files[0].name);
} }
const response = await fetch('?/url', { method: 'POST', body: data }); const response = await fetch(ROUTE_NAMES.UPLOAD_URL, { method: 'POST', body: data });
/** @type {import('@sveltejs/kit').ActionResult} */ /** @type {import('@sveltejs/kit').ActionResult} */
const result = deserialize(await response.text()); const result = deserialize(await response.text());
if (result.type === 'success') return result.data?.url; if (result.type === 'success') return result.data?.url;
@@ -151,7 +152,6 @@
return true; return true;
} }
// `/(angemeldet)/view` return true or false
async function checkVorgangExists(vorgangName: string) { async function checkVorgangExists(vorgangName: string) {
if (vorgangName == '') { if (vorgangName == '') {
vorgangPIN = vorgangPINOld; vorgangPIN = vorgangPINOld;
@@ -159,7 +159,8 @@
} }
try { try {
const url = `/api/list/${vorgangName}`; // `HEAD` method
const url = API_ROUTES.VORGANG_NAME_EXIST(vorgangName);
const response = await fetch(url, { method: 'HEAD' }); const response = await fetch(url, { method: 'HEAD' });
if (response.status === 200) { if (response.status === 200) {
@@ -185,7 +186,7 @@
async function getVorgangPIN(vorgangName: string) { async function getVorgangPIN(vorgangName: string) {
if (vorgangName == '') return; if (vorgangName == '') return;
let url = `/api/vorgang/${vorgangName}/vorgangPIN`; let url = API_ROUTES.VORGANG_PIN(vorgangName);
const response = await fetch(url); const response = await fetch(url);
if (response.status == 200) { if (response.status == 200) {

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import { API_ROUTES } from '../../index.js';
const { data } = $props(); const { data } = $props();
@@ -20,7 +21,7 @@
}); });
async function getUsers() { async function getUsers() {
const URL = '/api/users'; const URL = API_ROUTES.USERS;
try { try {
const response = await fetch(URL); const response = await fetch(URL);
@@ -42,7 +43,7 @@
return; return;
} }
const URL = '/api/users'; const URL = API_ROUTES.USERS;
const userData = { userName: userName, userPassword: userPassword }; const userData = { userName: userName, userPassword: userPassword };
try { try {
@@ -78,7 +79,7 @@
} }
async function deleteUser(userId: string) { async function deleteUser(userId: string) {
const URL = `/api/users/${userId}`; const URL = API_ROUTES.USER(userId);
try { try {
const response = await fetch(URL, { const response = await fetch(URL, {

View File

@@ -4,6 +4,7 @@ import {
} from '$lib/server/vorgangService'; } from '$lib/server/vorgangService';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './list/[vorgang]/$types'; import type { PageServerLoad } from './list/[vorgang]/$types';
import { ROUTE_NAMES } from '..';
export const load: PageServerLoad = async ({ params, url, locals }) => { export const load: PageServerLoad = async ({ params, url, locals }) => {
if (locals.user) { if (locals.user) {
@@ -18,5 +19,5 @@ export const load: PageServerLoad = async ({ params, url, locals }) => {
const isVorgangValid = vorgangExists(vorgangToken); const isVorgangValid = vorgangExists(vorgangToken);
const isVorgangPINValid = vorgangPINValidation(vorgangToken, vorgangPIN); const isVorgangPINValid = vorgangPINValidation(vorgangToken, vorgangPIN);
if (!isVorgangValid || !isVorgangPINValid) throw redirect(303, `/anmeldung?vorgang=${vorgangToken}`); if (!isVorgangValid || !isVorgangPINValid) throw redirect(303, ROUTE_NAMES.ANMELDUNG_VORGANG_PARAM(vorgangToken));
}; };

View File

@@ -12,6 +12,7 @@
import { invalidateAll } from '$app/navigation'; import { invalidateAll } from '$app/navigation';
import NameItemEditor from '$lib/components/NameItemEditor.svelte'; import NameItemEditor from '$lib/components/NameItemEditor.svelte';
import EmptyList from '$lib/components/EmptyList.svelte'; import EmptyList from '$lib/components/EmptyList.svelte';
import { API_ROUTES, ROUTE_NAMES } from '../../../index.js';
//Seite für die Tatort-Liste //Seite für die Tatort-Liste
let { data } = $props(); let { data } = $props();
@@ -44,7 +45,7 @@
inProgress = true; inProgress = true;
isError = false; isError = false;
try { try {
const res = await fetch(`/api/list/${vorgangToken}/${oldName}`, { const res = await fetch(API_ROUTES.CRIME(vorgangToken, oldName), {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -70,11 +71,10 @@
open = true; open = true;
inProgress = true; inProgress = true;
isError = false; isError = false;
let path = new URL(data.url).pathname; let path = API_ROUTES.CRIME(vorgangToken, tatort)
path += `/${tatort}`;
try { try {
const res = await fetch(`/api${path}`, { const res = await fetch(path, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -144,9 +144,9 @@ Mit freundlichen Grüßen,
<div class=" flex gap-x-4"> <div class=" flex gap-x-4">
<a <a
data-testid="crime-link" data-testid="crime-link"
href="/view/{vorgangToken}/{item.name}?pin={vorgangPIN}" href="{ROUTE_NAMES.CRIME(vorgangToken, item.name, vorgangPIN)}"
class=" flex justify-between gap-x-6 py-5" class=" flex justify-between gap-x-6 py-5"
aria-label="/view/{vorgangToken}/{item.name}?pin={vorgangPIN}" aria-label="{ROUTE_NAMES.CRIME(vorgangToken, item.name, vorgangPIN)}"
title={item.name} title={item.name}
> >
<Cube /> <Cube />

View File

@@ -1,9 +1,10 @@
import { API_ROUTES } from '../../../index.js';
export async function load({fetch, params, url}){ export async function load({fetch, params, url}){
const vorgangResponse = await fetch(`/api/list`); const vorgangResponse = await fetch(API_ROUTES.LIST);
const vorgangList = await vorgangResponse.json() const vorgangList = await vorgangResponse.json()
const vorgangToken = params.vorgang; const vorgangToken = params.vorgang;
const crimesListResponse = await fetch(`/api/list/${vorgangToken}`) const crimesListResponse = await fetch(API_ROUTES.VORGANG(vorgangToken))
const crimesList = await crimesListResponse.json(); const crimesList = await crimesListResponse.json();
const vorgang = vorgangList.find(v => v.vorgangToken === vorgangToken); //vorgang sollte ein eigener Typ werden, und dann kann man es hier vernünftig typisieren const vorgang = vorgangList.find(v => v.vorgangToken === vorgangToken); //vorgang sollte ein eigener Typ werden, und dann kann man es hier vernünftig typisieren
if(!vorgang || !crimesList){ if(!vorgang || !crimesList){

View File

@@ -1,5 +1,6 @@
import { loginUser, logoutUser } from '$lib/server/authService'; import { loginUser, logoutUser } from '$lib/server/authService';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import { ROUTE_NAMES } from '../index.js';
export const actions = { export const actions = {
login: ({ request, cookies }) => loginUser({ request, cookies }), login: ({ request, cookies }) => loginUser({ request, cookies }),
@@ -11,6 +12,6 @@ export const actions = {
if (!vorgangToken || !vorgangPIN) return; if (!vorgangToken || !vorgangPIN) return;
throw redirect(303, `/list/${vorgangToken}?pin=${vorgangPIN}`); throw redirect(303, ROUTE_NAMES.VORGANG(vorgangToken, vorgangPIN));
} }
} as const; } as const;

View File

@@ -13,6 +13,7 @@
export let open = false; export let open = false;
import { page } from '$app/state'; import { page } from '$app/state';
import { ROUTE_NAMES } from '../index.js';
const vorgangToken = page.url.searchParams.get('vorgang'); const vorgangToken = page.url.searchParams.get('vorgang');
</script> </script>
@@ -27,7 +28,7 @@
<div class="w-full max-w-sm mx-auto"> <div class="w-full max-w-sm mx-auto">
<div class="relative mt-5 bg-gray-50 rounded-xl shadow-xl p-3 pt-1"> <div class="relative mt-5 bg-gray-50 rounded-xl shadow-xl p-3 pt-1">
<div class="mt-10"> <div class="mt-10">
<form action="?/getVorgangByToken" method="POST"> <form action="{ROUTE_NAMES.ANMELDUNG_GET_VORGANG_BY_TOKEN}" method="POST">
<BaseInputField <BaseInputField
id="vorgang-token" id="vorgang-token"
name="vorgang-token" name="vorgang-token"
@@ -59,7 +60,7 @@
<Modal {open}> <Modal {open}>
<ModalTitle>Anmelden</ModalTitle> <ModalTitle>Anmelden</ModalTitle>
<ModalContent class="flex justify-center"> <ModalContent class="flex justify-center">
<form action="?/login" method="POST"> <form action="{ROUTE_NAMES.ANMELDUNG_LOGIN}" method="POST">
<div> <div>
<label for="user" class="text-sm font-medium leading-6 text-gray-900">Kennung</label> <label for="user" class="text-sm font-medium leading-6 text-gray-900">Kennung</label>
<div class="mt-2"> <div class="mt-2">

View File

@@ -1,16 +1,44 @@
export const ROUTE_NAMES = { export const ROUTE_NAMES = {
ROOT: '/', ROOT: '/',
// (angemeldet) // (angemeldet)
LIST: '/list', LIST: '/list',
UPLOAD: '/upload', UPLOAD: '/upload',
USERMGMT: '/user-management', // UPLOAD actions
// (token-based) UPLOAD_URL: '/upload?/url',
VORGANG: (vorgangToken) => `/list/${vorgangToken}`, UPLOAD_VALIDATE: '/upload?/validate',
CRIME: (vorgangToken, tatort) => `/view/${vorgangToken}/${tatort}`
USERMGMT: '/user-management',
// (token-based)
// `pin` param is optional
VORGANG: (vorgangToken: string, vorgangPIN: string) =>
vorgangPIN ? `/list/${vorgangToken}?pin=${vorgangPIN}` : `/list/${vorgangToken}`,
CRIME: (vorgangToken: string, tatort: string, vorgangPIN: string) =>
vorgangPIN
? `/view/${vorgangToken}/${tatort}?pin=${vorgangPIN}`
: `/view/${vorgangToken}/${tatort}`,
// Anmeldung: actions
ANMELDUNG: '/anmeldung',
ANMELDUNG_LOGIN: '/anmeldung?/login',
ANMELDUNG_LOGOUT: '/anmeldung?/logout',
ANMELDUNG_GET_VORGANG_BY_TOKEN: '/anmeldung?/getVorgangByToken',
ANMELDUNG_VORGANG_PARAM: (vorgangToken: string) => `/anmeldung?vorgang=${vorgangToken}`
}; };
export const API_ROUTES = { export const API_ROUTES = {
LIST: '/api/list', LIST: '/api/list',
VORGANG: (vorgangToken: string) => `/api/list/${vorgangToken}`, VORGANG: (vorgangToken: string) => `/api/list/${vorgangToken}`,
// via `HEAD` method
VORGANG_NAME_EXIST: (vorgangName: string) => `/api/list/${vorgangName}`,
VORGANG_PIN: (vorgangName: string) => `/api/vorgang/${vorgangName}/vorgangPIN`,
// Tatort
CRIME: (vorgangToken: string, crimeName: string) => `/api/list/${vorgangToken}/${crimeName}`,
// Users
USERS: '/api/users',
USER: (userId: string) => `/api/users/${userId}`
}; };

24
tests/Home.view.test.ts Normal file
View File

@@ -0,0 +1,24 @@
import { render, screen } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';
import HomePage from '../src/routes/(angemeldet)/+page.svelte';
import { ROUTE_NAMES } from '../src/routes';
import { baseData } from './fixtures';
describe('Home-Page View', () => {
it('Überprüfe Links', () => {
render(HomePage, { props: { data: baseData } });
let linkElement = screen.getByText('Vorgänge');
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', ROUTE_NAMES.LIST);
linkElement = screen.getByText('Hinzufügen');
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', ROUTE_NAMES.UPLOAD);
linkElement = screen.getByText('Benutzerverwaltung');
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', ROUTE_NAMES.USERMGMT);
});
});

29
tests/Layout.test.ts Normal file
View File

@@ -0,0 +1,29 @@
import { describe, test, expect } from 'vitest';
import { load } from '../src/routes/(angemeldet)/+layout.server';
import { ROUTE_NAMES } from '../src/routes';
import { baseData, mockEvent } from './fixtures';
describe('+layout.server load(): Teste korrekte URL', () => {
test('Werfe redirect zu /anmeldung wenn User nicht eingeloggt', async () => {
const mockEvent = {
locals: {
user: null
},
url: new URL(`https://example.com/not-anmeldung`)
};
try {
load(mockEvent);
throw new Error('Expected load() to throw');
} catch (err) {
expect(err.status).toBe(303);
expect(err.location).toBe(ROUTE_NAMES.ANMELDUNG);
}
});
});
describe('+layout.server load(): Teste erfolgreichen Pfad', () => {
test('Werfe kein Fehler', async () => {
const result = load(mockEvent);
expect(result).toEqual({ user: baseData.user });
});
});

View File

@@ -4,6 +4,7 @@ import * as nav from '$app/navigation';
import TatortListPage from '../src/routes/(token-based)/list/[vorgang]/+page.svelte'; import TatortListPage from '../src/routes/(token-based)/list/[vorgang]/+page.svelte';
import { baseData } from './fixtures'; import { baseData } from './fixtures';
import { tick } from 'svelte'; import { tick } from 'svelte';
import { API_ROUTES } from '../src/routes';
vi.spyOn(nav, 'invalidateAll').mockResolvedValue(); vi.spyOn(nav, 'invalidateAll').mockResolvedValue();
global.fetch = vi.fn().mockResolvedValue({ ok: true }); global.fetch = vi.fn().mockResolvedValue({ ok: true });
@@ -50,6 +51,7 @@ describe('Seite: Vorgangsansicht', () => {
it('führt DELETE-Request aus und entfernt Element aus UI', async () => { it('führt DELETE-Request aus und entfernt Element aus UI', async () => {
const testData = structuredClone(baseData); const testData = structuredClone(baseData);
const oldName = testData.crimesList[0].name; const oldName = testData.crimesList[0].name;
const vorgang = testData.vorgang;
render(TatortListPage, { props: { data: testData } }); render(TatortListPage, { props: { data: testData } });
const initialItems = screen.getAllByTestId('test-list-item'); const initialItems = screen.getAllByTestId('test-list-item');
@@ -62,10 +64,9 @@ describe('Seite: Vorgangsansicht', () => {
await fireEvent.click(within(listItem).getByTestId('delete-button')); await fireEvent.click(within(listItem).getByTestId('delete-button'));
await tick(); await tick();
let expectedPath = new URL(testData.url).pathname; let expectedPath = API_ROUTES.CRIME(vorgang.vorgangToken, oldName)
expectedPath += `/${oldName}`
expect(global.fetch).toHaveBeenCalledWith( expect(global.fetch).toHaveBeenCalledWith(
`/api${expectedPath}`, expectedPath,
expect.objectContaining({ expect.objectContaining({
method: 'DELETE', method: 'DELETE',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },

View File

@@ -2,6 +2,7 @@ import { render, screen, within } from '@testing-library/svelte';
import { describe, expect, it, test } from 'vitest'; import { describe, expect, it, test } from 'vitest';
import TatortListPage from '../src/routes/(token-based)/list/[vorgang]/+page.svelte'; import TatortListPage from '../src/routes/(token-based)/list/[vorgang]/+page.svelte';
import { baseData } from './fixtures'; import { baseData } from './fixtures';
import { ROUTE_NAMES } from '../src/routes';
describe('Seite: Vorgangsansicht', () => { describe('Seite: Vorgangsansicht', () => {
test.todo('zeigt PIN und Share-Link, wenn Admin'); test.todo('zeigt PIN und Share-Link, wenn Admin');
@@ -46,7 +47,7 @@ describe('Seite: Vorgangsansicht', () => {
items.forEach((item, i) => { items.forEach((item, i) => {
const link = within(item).getByRole('link'); const link = within(item).getByRole('link');
const expectedHref = `/view/${testData.vorgang.vorgangToken}/${testData.crimesList[i].name}?pin=${testData.vorgang.vorgangPIN}`; const expectedHref = ROUTE_NAMES.CRIME(testData.vorgang.vorgangToken, testData.crimesList[i].name, testData.vorgang.vorgangPIN);
expect(link).toBeInTheDocument(); expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', expectedHref); expect(link).toHaveAttribute('href', expectedHref);
@@ -84,4 +85,19 @@ describe('Seite: Vorgangsansicht', () => {
test.todo('zeigt keinen Share-Link oder PIN'); test.todo('zeigt keinen Share-Link oder PIN');
}); });
describe('Teste Links auf Korrektheit', () => {
it('Überprüfe Links', () => {
const crimesListOneItem = baseData.crimesList.slice(0, 1);
const crimeObj = crimesListOneItem[0];
const vorgObj = baseData.vorgangList[0]
const expectedURL = ROUTE_NAMES.CRIME(vorgObj.vorgangToken, crimeObj.name, vorgObj.vorgangPIN)
render(TatortListPage, { props: { data: { ...baseData, crimesList: crimesListOneItem } } });
const listItem = screen.getByTestId("test-list-item");
const linkElement = within(listItem).getByRole('link');
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', expectedURL);
});
});
}); });

View File

@@ -1,7 +1,8 @@
import { render } from '@testing-library/svelte'; import { render, screen, within } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import VorgangListPage from '../src/routes/(angemeldet)/list/+page.svelte'; import VorgangListPage from '../src/routes/(angemeldet)/list/+page.svelte';
import { baseData } from './fixtures'; import { baseData } from './fixtures';
import { ROUTE_NAMES } from '../src/routes';
describe('Vorgänge Liste Page EmptyList-Komponente View', () => { describe('Vorgänge Liste Page EmptyList-Komponente View', () => {
it('zeigt EmptyList-Komponente an, wenn Liste leer ist', () => { it('zeigt EmptyList-Komponente an, wenn Liste leer ist', () => {
@@ -19,3 +20,17 @@ describe('Vorgänge Liste Page EmptyList-Komponente View', () => {
expect(items.length).toBeGreaterThan(0); expect(items.length).toBeGreaterThan(0);
}); });
}); });
describe('Teste Links auf Korrektheit', () => {
it('Überprüfe Links', () => {
const vorgListOneItem = baseData.vorgangList.slice(0, 1);
const vorgObj = vorgListOneItem[0];
const expectedURL = ROUTE_NAMES.VORGANG(vorgObj.vorgangToken, vorgObj.vorgangPIN)
render(VorgangListPage, { props: { data: { ...baseData, vorgangList: vorgListOneItem } } });
const listItem = screen.getByTestId("test-list-item");
const linkElement = within(listItem).getByRole('link');
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', expectedURL);
});
});

View File

@@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/svelte';
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { ROUTE_NAMES } from '../../src/routes'; import { ROUTE_NAMES } from '../../src/routes';
import { baseData } from '../fixtures';
import Footer from '$lib/components/Footer.svelte'; import Footer from '$lib/components/Footer.svelte';
@@ -19,13 +20,8 @@ describe('Footer component', () => {
expect(linkElement).toHaveAttribute('href', ROUTE_NAMES.ROOT); expect(linkElement).toHaveAttribute('href', ROUTE_NAMES.ROOT);
}); });
test('Enthält Profil-Icon und entsprechenden Link: angemeldet', () => { test('Enthält Profil-Icon und entsprechenden Link: angemeldet', () => {
const mockData = { render(Footer, { props: { data: baseData } });
user: { const linkElement = screen.getByText('admin');
id: 'admin'
}
};
render(Footer, { props: { data: mockData } });
const linkElement = screen.getByText('admin', { exact: false });
expect(linkElement).toBeInTheDocument(); expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', ROUTE_NAMES.ROOT); expect(linkElement).toHaveAttribute('href', ROUTE_NAMES.ROOT);

View File

@@ -0,0 +1,22 @@
import { render, screen } from '@testing-library/svelte';
import { describe, test, expect } from 'vitest';
import { ROUTE_NAMES } from '../../src/routes';
import { baseData } from '../fixtures';
import Header from '$lib/components/Header.svelte';
describe('Header component', () => {
test('Enthält Landeswappen von NDS und entsprechenden Link', () => {
render(Header, { props: { data: baseData } });
const linkElement = screen.getByText('Tatort Niedersachen').closest('a');
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', ROUTE_NAMES.ROOT);
});
test('Form enthält korrekten Link', () => {
const { container } = render(Header, { props: { data: baseData } });
const formElement = container.querySelector('form');
expect(formElement).toBeInTheDocument();
expect(formElement).toHaveAttribute('action', ROUTE_NAMES.ANMELDUNG_LOGOUT);
});
});

View File

@@ -41,6 +41,13 @@ export const baseData = {
vorgang: testVorgangsList[0], vorgang: testVorgangsList[0],
vorgangList: testVorgangsList, vorgangList: testVorgangsList,
crimesList: testCrimesList, crimesList: testCrimesList,
url: `https://example.com/${testVorgangsList[0].vorgangToken}`, url: `https://example.com/list/${testVorgangsList[0].vorgangToken}`,
crimeNames: ['modell-A', 'Fall-A'] crimeNames: ['modell-A', 'Fall-A']
}; };
export const mockEvent = {
locals: {
user: baseData.user
},
url: new URL(`https://example.com/anmeldung`)
};