feat(nextcloud-status): Cloud-Formular zeigt nur deutsche Meldungen, Bildadresse auch per http (j9f)
- Vorabpruefung im Browser, API-Kennungen werden zu de/en-Texten - Beschriftung Bildadresse mit neuem Hinweis zu http und https Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { NextcloudFormError } from '@/lib/nextcloud-status-api';
|
||||
import {
|
||||
CLOUD_FORM_ERROR_CODES,
|
||||
cloudFormErrorKey,
|
||||
isAllowedLogoFileType,
|
||||
validateCloudForm,
|
||||
} from './cloud-form-errors';
|
||||
|
||||
const OK = {
|
||||
customerName: 'Kunde',
|
||||
baseUrl: 'https://cloud.example.de',
|
||||
logoMode: 'none' as const,
|
||||
logoUrl: '',
|
||||
};
|
||||
|
||||
describe('validateCloudForm', () => {
|
||||
it('gueltige Eingaben -> null', () => {
|
||||
expect(validateCloudForm(OK)).toBeNull();
|
||||
});
|
||||
|
||||
it('Name: leer und zu lang', () => {
|
||||
expect(validateCloudForm({ ...OK, customerName: ' ' })).toBe('customerNameRequired');
|
||||
expect(validateCloudForm({ ...OK, customerName: 'x'.repeat(121) })).toBe('customerNameTooLong');
|
||||
});
|
||||
|
||||
it('Adresse: leer, ohne Schema, falsches Schema', () => {
|
||||
expect(validateCloudForm({ ...OK, baseUrl: '' })).toBe('baseUrlRequired');
|
||||
expect(validateCloudForm({ ...OK, baseUrl: 'cloud.example.de' })).toBe('baseUrlInvalid');
|
||||
expect(validateCloudForm({ ...OK, baseUrl: 'ftp://x' })).toBe('baseUrlInvalid');
|
||||
});
|
||||
|
||||
it('Bildadresse nur im Modus url geprueft', () => {
|
||||
for (const logoUrl of ['ftp://x', 'kein link', 'http://u:p@a.de/x.png']) {
|
||||
expect(validateCloudForm({ ...OK, logoMode: 'url', logoUrl })).toBe('logoUrlInvalid');
|
||||
}
|
||||
for (const logoUrl of ['http://logo.example.de/a.png', 'https://logo.example.de/a.png', '']) {
|
||||
expect(validateCloudForm({ ...OK, logoMode: 'url', logoUrl })).toBeNull();
|
||||
}
|
||||
expect(validateCloudForm({ ...OK, logoMode: 'none', logoUrl: 'ftp://x' })).toBeNull();
|
||||
expect(validateCloudForm({ ...OK, logoMode: 'upload', logoUrl: 'ftp://x' })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAllowedLogoFileType', () => {
|
||||
it('nur PNG, JPEG, GIF, WebP', () => {
|
||||
for (const t of ['image/png', 'image/jpeg', 'image/gif', 'image/webp']) {
|
||||
expect(isAllowedLogoFileType(t)).toBe(true);
|
||||
}
|
||||
expect(isAllowedLogoFileType('image/svg+xml')).toBe(false);
|
||||
expect(isAllowedLogoFileType('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cloudFormErrorKey', () => {
|
||||
it('bekannte Kennungen -> errors.<code>', () => {
|
||||
for (const code of CLOUD_FORM_ERROR_CODES) {
|
||||
expect(cloudFormErrorKey(new NextcloudFormError(400, code, 'x'), 'saveError')).toBe(
|
||||
`errors.${code}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('413 und 404 ohne Kennung', () => {
|
||||
expect(
|
||||
cloudFormErrorKey(new NextcloudFormError(413, null, 'File too large'), 'saveError'),
|
||||
).toBe('errors.logoFileTooLarge');
|
||||
expect(cloudFormErrorKey(new NextcloudFormError(404, null, 'Not Found'), 'deleteError')).toBe(
|
||||
'errors.notFound',
|
||||
);
|
||||
});
|
||||
|
||||
it('unbekannte Kennung, englischer Text oder einfacher Fehler -> allgemeiner Text', () => {
|
||||
expect(cloudFormErrorKey(new NextcloudFormError(400, 'whatever', 'x'), 'saveError')).toBe(
|
||||
'saveError',
|
||||
);
|
||||
expect(
|
||||
cloudFormErrorKey(
|
||||
new NextcloudFormError(400, null, 'logoUrl must be a URL address'),
|
||||
'saveError',
|
||||
),
|
||||
).toBe('saveError');
|
||||
expect(cloudFormErrorKey(new Error('boom'), 'deleteError')).toBe('deleteError');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import { NextcloudFormError } from '@/lib/nextcloud-status-api';
|
||||
|
||||
/**
|
||||
* Fehlertexte des Cloud-Formulars (quick-261008-j9f, D-05): vorab pruefen und
|
||||
* Fehlerkennungen der API in Uebersetzungsschluessel abbilden. Die Oberflaeche
|
||||
* zeigt nie den Text des Servers. Die Kennungen entsprechen dem Katalog in
|
||||
* `apps/api/src/nextcloud-status/nextcloud-form-errors.ts`.
|
||||
*/
|
||||
export const CLOUD_FORM_ERROR_CODES = [
|
||||
'customerNameRequired',
|
||||
'customerNameTooLong',
|
||||
'baseUrlRequired',
|
||||
'baseUrlInvalid',
|
||||
'logoUrlInvalid',
|
||||
'logoFileInvalid',
|
||||
'logoFileTooLarge',
|
||||
'logoFetchInternal',
|
||||
'logoFetchUnreachable',
|
||||
'logoFetchNotImage',
|
||||
'logoFetchTooLarge',
|
||||
'notFound',
|
||||
] as const;
|
||||
|
||||
export type CloudFormErrorCode = (typeof CLOUD_FORM_ERROR_CODES)[number];
|
||||
|
||||
const MAX_NAME = 120;
|
||||
const MAX_URL = 2048;
|
||||
|
||||
/** http(s)-Adresse ohne Zugangsdaten und mit Rechnername. */
|
||||
function isHttpUrl(raw: string): boolean {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(raw);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
(url.protocol === 'http:' || url.protocol === 'https:') &&
|
||||
!url.username &&
|
||||
!url.password &&
|
||||
url.hostname !== ''
|
||||
);
|
||||
}
|
||||
|
||||
export interface CloudFormInput {
|
||||
customerName: string;
|
||||
baseUrl: string;
|
||||
logoMode: 'none' | 'upload' | 'url';
|
||||
logoUrl: string;
|
||||
}
|
||||
|
||||
/** Erster Fehler in der Reihenfolge Name, Adresse, Bildadresse — oder `null`. */
|
||||
export function validateCloudForm(input: CloudFormInput): CloudFormErrorCode | null {
|
||||
const name = input.customerName.trim();
|
||||
if (!name) return 'customerNameRequired';
|
||||
if (name.length > MAX_NAME) return 'customerNameTooLong';
|
||||
|
||||
const base = input.baseUrl.trim();
|
||||
if (!base) return 'baseUrlRequired';
|
||||
if (base.length > MAX_URL || !isHttpUrl(base)) return 'baseUrlInvalid';
|
||||
|
||||
if (input.logoMode === 'url') {
|
||||
const logo = input.logoUrl.trim();
|
||||
if (logo && (logo.length > MAX_URL || !isHttpUrl(logo))) return 'logoUrlInvalid';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const ALLOWED_LOGO_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
|
||||
|
||||
/** Dateitypen, die die API als Logo annimmt (SVG nie). */
|
||||
export function isAllowedLogoFileType(type: string): boolean {
|
||||
return ALLOWED_LOGO_TYPES.has(type);
|
||||
}
|
||||
|
||||
function isKnownCode(code: string | null): code is CloudFormErrorCode {
|
||||
return code !== null && (CLOUD_FORM_ERROR_CODES as readonly string[]).includes(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uebersetzungsschluessel (unter `nextcloudStatus.form`) zu einem Fehler:
|
||||
* bekannte Kennung -> `errors.<code>`, 413 -> Dateigroesse, 404 -> nicht
|
||||
* gefunden, sonst der allgemeine Text (`saveError` / `deleteError`).
|
||||
*/
|
||||
export function cloudFormErrorKey(err: unknown, fallback: 'saveError' | 'deleteError'): string {
|
||||
if (err instanceof NextcloudFormError) {
|
||||
if (isKnownCode(err.code)) return `errors.${err.code}`;
|
||||
if (err.status === 413) return 'errors.logoFileTooLarge';
|
||||
if (err.status === 404) return 'errors.notFound';
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
Reference in New Issue
Block a user