feat(instanz): Instanzsteuerung mit serverseitigem Riegel
INSTANZ entscheidet, welche Kalenderanbieter, Postfächer und Integrationen es gibt. Was eine Instanz nicht darf, liefert dort 404 statt eines Hinweises. Ein falsch geschriebener Wert bricht ab, statt still auf einen Standard zu fallen. Die Konfiguration wird mit zod geprüft; Fehlermeldungen nennen den Variablennamen, nie den Wert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
748d032f70
commit
e2b00f4a57
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
FAEHIGKEITEN,
|
||||
INSTANZEN,
|
||||
faehigkeitenVon,
|
||||
istErlaubt,
|
||||
istInstanz,
|
||||
kalenderAnbieterVon,
|
||||
leseInstanz,
|
||||
} from './instanz'
|
||||
|
||||
describe('leseInstanz', () => {
|
||||
it('nimmt die erlaubten Werte an', () => {
|
||||
expect(leseInstanz('firma')).toBe('firma')
|
||||
expect(leseInstanz('daheim')).toBe('daheim')
|
||||
})
|
||||
|
||||
it('ist unempfindlich gegen Gross- und Kleinschreibung und Leerzeichen', () => {
|
||||
expect(leseInstanz(' FIRMA ')).toBe('firma')
|
||||
})
|
||||
|
||||
it('bricht ab, wenn nichts gesetzt ist', () => {
|
||||
expect(() => leseInstanz(undefined)).toThrow(/nicht gesetzt/)
|
||||
expect(() => leseInstanz(' ')).toThrow(/nicht gesetzt/)
|
||||
})
|
||||
|
||||
it('faellt bei einem Tippfehler nicht still auf einen Standard zurueck', () => {
|
||||
expect(() => leseInstanz('fimra')).toThrow(/unbekannten Wert/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('istInstanz', () => {
|
||||
it('erkennt gueltige Werte', () => {
|
||||
expect(istInstanz('firma')).toBe(true)
|
||||
expect(istInstanz('daheim')).toBe(true)
|
||||
})
|
||||
|
||||
it('weist alles andere ab', () => {
|
||||
expect(istInstanz('privat')).toBe(false)
|
||||
expect(istInstanz(undefined)).toBe(false)
|
||||
expect(istInstanz(42)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('istErlaubt', () => {
|
||||
it('trennt die Kalenderanbieter der beiden Instanzen', () => {
|
||||
expect(istErlaubt('firma', 'kalender:graph')).toBe(true)
|
||||
expect(istErlaubt('firma', 'kalender:google')).toBe(false)
|
||||
expect(istErlaubt('daheim', 'kalender:google')).toBe(true)
|
||||
})
|
||||
|
||||
it('haelt die Integrationen auseinander', () => {
|
||||
expect(istErlaubt('firma', 'integration:zammad')).toBe(true)
|
||||
expect(istErlaubt('daheim', 'integration:zammad')).toBe(false)
|
||||
expect(istErlaubt('daheim', 'integration:telegram')).toBe(true)
|
||||
expect(istErlaubt('firma', 'integration:telegram')).toBe(false)
|
||||
})
|
||||
|
||||
it('richtet den Spiegel in genau eine Richtung ein', () => {
|
||||
expect(istErlaubt('firma', 'spiegel:senden')).toBe(true)
|
||||
expect(istErlaubt('firma', 'spiegel:empfangen')).toBe(false)
|
||||
expect(istErlaubt('daheim', 'spiegel:empfangen')).toBe(true)
|
||||
expect(istErlaubt('daheim', 'spiegel:senden')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('faehigkeitenVon', () => {
|
||||
it('liefert nur bekannte Faehigkeiten', () => {
|
||||
for (const instanz of INSTANZEN) {
|
||||
for (const faehigkeit of faehigkeitenVon(instanz)) {
|
||||
expect(FAEHIGKEITEN).toContain(faehigkeit)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('gibt keiner Instanz alles', () => {
|
||||
for (const instanz of INSTANZEN) {
|
||||
expect(faehigkeitenVon(instanz).length).toBeLessThan(FAEHIGKEITEN.length)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('kalenderAnbieterVon', () => {
|
||||
it('nennt fuer die Firma nur Graph', () => {
|
||||
expect(kalenderAnbieterVon('firma')).toEqual(['GRAPH'])
|
||||
})
|
||||
|
||||
it('nennt daheim beide', () => {
|
||||
expect(kalenderAnbieterVon('daheim')).toEqual(['GRAPH', 'GOOGLE'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Instanzsteuerung.
|
||||
*
|
||||
* Ein Image, zwei Deployments. Welche Provider, Integrationen und Routen
|
||||
* erlaubt sind, haengt allein an INSTANZ und wird serverseitig erzwungen –
|
||||
* nicht in der Oberflaeche versteckt.
|
||||
*
|
||||
* Diese Datei ist reine Logik: kein Zugriff auf process.env, keine Systemzeit.
|
||||
*/
|
||||
|
||||
export const INSTANZEN = ['firma', 'daheim'] as const
|
||||
export type Instanz = (typeof INSTANZEN)[number]
|
||||
|
||||
/**
|
||||
* Alles, was je nach Instanz an- oder abgeschaltet ist. Eine Faehigkeit, die
|
||||
* hier nicht steht, gibt es ueberall.
|
||||
*/
|
||||
export const FAEHIGKEITEN = [
|
||||
// Kalender- und Mailanbindung
|
||||
'kalender:graph',
|
||||
'kalender:google',
|
||||
'mail:graph',
|
||||
'mail:gmail',
|
||||
// Integrationen
|
||||
'integration:zammad',
|
||||
'integration:wartungsplaner',
|
||||
'integration:telegram',
|
||||
'spiegel:senden',
|
||||
'spiegel:empfangen',
|
||||
] as const
|
||||
export type Faehigkeit = (typeof FAEHIGKEITEN)[number]
|
||||
|
||||
const ERLAUBT: Readonly<Record<Instanz, readonly Faehigkeit[]>> = {
|
||||
firma: [
|
||||
'kalender:graph',
|
||||
'mail:graph',
|
||||
'integration:zammad',
|
||||
'integration:wartungsplaner',
|
||||
'spiegel:senden',
|
||||
],
|
||||
daheim: [
|
||||
'kalender:graph',
|
||||
'kalender:google',
|
||||
'mail:graph',
|
||||
'mail:gmail',
|
||||
'integration:telegram',
|
||||
'spiegel:empfangen',
|
||||
],
|
||||
}
|
||||
|
||||
/** Anzeigename der Instanz fuer die Oberflaeche. */
|
||||
export const INSTANZ_NAME: Readonly<Record<Instanz, string>> = {
|
||||
firma: 'Firma',
|
||||
daheim: 'Daheim',
|
||||
}
|
||||
|
||||
export function istInstanz(wert: unknown): wert is Instanz {
|
||||
return typeof wert === 'string' && (INSTANZEN as readonly string[]).includes(wert)
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest den Konfigurationswert und bricht bei Unsinn ab. Ein falsch
|
||||
* geschriebenes INSTANZ darf nicht stillschweigend zu "daheim" werden –
|
||||
* sonst laufen in der Firma Provider an, die dort nichts zu suchen haben.
|
||||
*/
|
||||
export function leseInstanz(wert: string | undefined): Instanz {
|
||||
const bereinigt = wert?.trim().toLowerCase()
|
||||
if (!bereinigt) {
|
||||
throw new Error('INSTANZ ist nicht gesetzt. Erlaubt: ' + INSTANZEN.join(', '))
|
||||
}
|
||||
if (!istInstanz(bereinigt)) {
|
||||
throw new Error(
|
||||
`INSTANZ hat den unbekannten Wert "${wert ?? ''}". Erlaubt: ` +
|
||||
INSTANZEN.join(', '),
|
||||
)
|
||||
}
|
||||
return bereinigt
|
||||
}
|
||||
|
||||
/** Ist diese Faehigkeit in dieser Instanz erlaubt? */
|
||||
export function istErlaubt(instanz: Instanz, faehigkeit: Faehigkeit): boolean {
|
||||
return ERLAUBT[instanz].includes(faehigkeit)
|
||||
}
|
||||
|
||||
/** Alle in dieser Instanz erlaubten Faehigkeiten, in fester Reihenfolge. */
|
||||
export function faehigkeitenVon(instanz: Instanz): readonly Faehigkeit[] {
|
||||
return FAEHIGKEITEN.filter((f) => istErlaubt(instanz, f))
|
||||
}
|
||||
|
||||
/** Welche Kalenderanbieter darf diese Instanz verbinden? */
|
||||
export function kalenderAnbieterVon(instanz: Instanz): readonly ('GRAPH' | 'GOOGLE')[] {
|
||||
const anbieter: ('GRAPH' | 'GOOGLE')[] = []
|
||||
if (istErlaubt(instanz, 'kalender:graph')) anbieter.push('GRAPH')
|
||||
if (istErlaubt(instanz, 'kalender:google')) anbieter.push('GOOGLE')
|
||||
return anbieter
|
||||
}
|
||||
Reference in New Issue
Block a user