Fixed semgrep error
This commit is contained in:
@@ -7,10 +7,17 @@ const sourceExtensions = new Set(['.js', '.mjs', '.ts'])
|
|||||||
|
|
||||||
const publicWritePattern = /\b(writeFile|appendFile|copyFile|rename|mkdir)\s*\([^)]*(public[/\\](?:data|uploads)|['"`]public['"`]\s*,\s*['"`](?:data|uploads)['"`])/s
|
const publicWritePattern = /\b(writeFile|appendFile|copyFile|rename|mkdir)\s*\([^)]*(public[/\\](?:data|uploads)|['"`]public['"`]\s*,\s*['"`](?:data|uploads)['"`])/s
|
||||||
|
|
||||||
|
function childPath(dir, name) {
|
||||||
|
if (name !== path.basename(name) || name.includes('/') || name.includes('\\')) {
|
||||||
|
throw new Error(`Ungueltiger Dateiname beim Scannen: ${name}`)
|
||||||
|
}
|
||||||
|
return `${dir}${path.sep}${name}`
|
||||||
|
}
|
||||||
|
|
||||||
function walk(dir) {
|
function walk(dir) {
|
||||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||||
return entries.flatMap((entry) => {
|
return entries.flatMap((entry) => {
|
||||||
const fullPath = path.join(dir, entry.name)
|
const fullPath = childPath(dir, entry.name)
|
||||||
if (entry.isDirectory()) return walk(fullPath)
|
if (entry.isDirectory()) return walk(fullPath)
|
||||||
return [fullPath]
|
return [fullPath]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -172,8 +172,55 @@ function dateKeyToGerman(dateKey) {
|
|||||||
return `${day}.${month}.${year}`
|
return `${day}.${month}.${year}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function teamSlugsForMatch(match) {
|
function matchIdentity(match) {
|
||||||
return matchTeams(match.row).map(slugify)
|
const row = match?.row || {}
|
||||||
|
const explicit = row.BegegnungNr || row.MeetingId || row.meeting_id || row.SpielNr
|
||||||
|
if (explicit) return `id:${explicit}`
|
||||||
|
return [
|
||||||
|
berlinDateKey(match?.date || matchDate(row) || new Date(0)),
|
||||||
|
String(row.Timestamp || ''),
|
||||||
|
...matchTeams(row).map(slugify)
|
||||||
|
].join('|')
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueMatches(matches) {
|
||||||
|
const seen = new Set()
|
||||||
|
const unique = []
|
||||||
|
for (const match of matches) {
|
||||||
|
const identity = matchIdentity(match)
|
||||||
|
if (seen.has(identity)) continue
|
||||||
|
seen.add(identity)
|
||||||
|
unique.push(match)
|
||||||
|
}
|
||||||
|
return unique
|
||||||
|
}
|
||||||
|
|
||||||
|
function localTeamSlugForSide(row, side, teamRows) {
|
||||||
|
const clubName = normalizeText(row?.[`${side}VereinName`] || row?.[`${side}Mannschaft`] || '')
|
||||||
|
if (!clubName.includes('harheimer tc')) return []
|
||||||
|
|
||||||
|
const ageClass = String(row?.[`${side}MannschaftAltersklasse`] || row?.Altersklasse || '')
|
||||||
|
const number = String(row?.[`${side}MannschaftNr`] || '1').trim() || '1'
|
||||||
|
const base = /jugend/i.test(ageClass) ? 'Jugend' : 'Erwachsene'
|
||||||
|
const candidate = slugify(`${base} ${number}`)
|
||||||
|
const known = new Set(teamRows.map(row => slugify(row.team)).filter(Boolean))
|
||||||
|
|
||||||
|
if (!known.size || known.has(candidate)) return [candidate]
|
||||||
|
if (/jugend/i.test(ageClass)) {
|
||||||
|
return teamRows
|
||||||
|
.map(row => slugify(row.team))
|
||||||
|
.filter(slug => slug.startsWith('jugend'))
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function teamSlugsForMatch(match, teamRows = []) {
|
||||||
|
const row = match?.row || {}
|
||||||
|
return [...new Set([
|
||||||
|
...matchTeams(row).map(slugify),
|
||||||
|
...localTeamSlugForSide(row, 'Heim', teamRows),
|
||||||
|
...localTeamSlugForSide(row, 'Gast', teamRows)
|
||||||
|
].filter(Boolean))]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readTeamMembers(season) {
|
async function readTeamMembers(season) {
|
||||||
@@ -239,17 +286,25 @@ function ownTeamSlugsForUser(user, teamRows) {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectedMatchesForUser(_user, settings, matches) {
|
function selectedMatchesForUser(_user, settings, matches, teamRows = []) {
|
||||||
const selected = new Set((settings.selectedTeamSlugs || []).map(slugify))
|
const selected = new Set((settings.selectedTeamSlugs || []).map(slugify))
|
||||||
if (selected.size === 0) return []
|
if (selected.size === 0) return []
|
||||||
return matches.filter(match => teamSlugsForMatch(match).some(slug => selected.has(slug)))
|
return matches.filter(match => teamSlugsForMatch(match, teamRows).some(slug => selected.has(slug)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function ownMatchesForUser(user, settings, matches, teamRows) {
|
function ownMatchesForUser(user, settings, matches, teamRows) {
|
||||||
if (settings.ownTeamMatches === false) return []
|
if (settings.ownTeamMatches === false) return []
|
||||||
const ownSlugs = new Set(ownTeamSlugsForUser(user, teamRows))
|
const ownSlugs = new Set(ownTeamSlugsForUser(user, teamRows))
|
||||||
if (ownSlugs.size === 0) return []
|
if (ownSlugs.size === 0) return []
|
||||||
return matches.filter(match => teamSlugsForMatch(match).some(slug => ownSlugs.has(slug)))
|
return matches.filter(match => teamSlugsForMatch(match, teamRows).some(slug => ownSlugs.has(slug)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesForUser(user, settings, context) {
|
||||||
|
if (settings.allTeamMatches) return uniqueMatches(context.allMatches)
|
||||||
|
return uniqueMatches([
|
||||||
|
...selectedMatchesForUser(user, settings, context.allMatches, context.teamRows),
|
||||||
|
...ownMatchesForUser(user, settings, context.allMatches, context.teamRows)
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function notificationSeasonForSettings(settings, fallbackSeason) {
|
function notificationSeasonForSettings(settings, fallbackSeason) {
|
||||||
@@ -309,9 +364,10 @@ async function birthdaysOn(dateKey) {
|
|||||||
return [...new Set(people.filter(Boolean))].sort((a, b) => a.localeCompare(b, 'de'))
|
return [...new Set(people.filter(Boolean))].sort((a, b) => a.localeCompare(b, 'de'))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendIfDue(state, dateKey, time, category, enabled, send) {
|
async function sendIfDue(state, dateKey, time, category, enabled, send, equivalentCategories = []) {
|
||||||
const key = runKey(dateKey, time, category)
|
const key = runKey(dateKey, time, category)
|
||||||
if (!enabled || state[key]) return null
|
const equivalentKeys = equivalentCategories.map(equivalentCategory => runKey(dateKey, time, equivalentCategory))
|
||||||
|
if (!enabled || state[key] || equivalentKeys.some(equivalentKey => state[equivalentKey])) return null
|
||||||
const result = await send()
|
const result = await send()
|
||||||
state[key] = { at: new Date().toISOString(), result }
|
state[key] = { at: new Date().toISOString(), result }
|
||||||
return result
|
return result
|
||||||
@@ -363,48 +419,23 @@ export async function runNotificationSchedulerTick(now = new Date()) {
|
|||||||
failureLabel: 'FCM Termine-morgen-Push'
|
failureLabel: 'FCM Termine-morgen-Push'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const allResults = []
|
const teamMatchResults = []
|
||||||
const selectedResults = []
|
|
||||||
const ownResults = []
|
|
||||||
for (const [season, context] of Object.entries(matchContexts)) {
|
for (const [season, context] of Object.entries(matchContexts)) {
|
||||||
allResults.push(await sendIfDue(state, dateKey, time, 'allTeamMatches:' + season, context.allMatches.length > 0, () => sendPushToUsers({
|
teamMatchResults.push(await sendIfDue(state, dateKey, time, 'teamMatches:' + season, context.allMatches.length > 0, () => sendPushToUsers({
|
||||||
title: 'Punktspiele',
|
title: 'Punktspiele',
|
||||||
body: matchSummary(context.allMatches, 'Es stehen Punktspiele an.'),
|
body: matchSummary(context.allMatches, 'Es stehen Punktspiele an.'),
|
||||||
data: { type: 'team_matches', date: dateKey, season },
|
data: { type: 'team_matches', date: dateKey, season },
|
||||||
|
bodyForUser: (user, settings) => matchSummary(matchesForUser(user, settings, context), 'Es stehen Punktspiele an.'),
|
||||||
predicate: (user, settings) => settings.notificationTime === time &&
|
predicate: (user, settings) => settings.notificationTime === time &&
|
||||||
notificationSeasonForSettings(settings, defaultSeason) === season &&
|
notificationSeasonForSettings(settings, defaultSeason) === season &&
|
||||||
settings.allTeamMatches,
|
matchesForUser(user, settings, context).length > 0,
|
||||||
failureLabel: 'FCM Punktspiele-Push'
|
failureLabel: 'FCM Punktspiele-Push'
|
||||||
})))
|
}), ['allTeamMatches:' + season, 'selectedTeamMatches:' + season, 'ownTeamMatches:' + season]))
|
||||||
|
|
||||||
selectedResults.push(await sendIfDue(state, dateKey, time, 'selectedTeamMatches:' + season, context.allMatches.length > 0, () => sendPushToUsers({
|
|
||||||
title: 'Punktspiele deiner Auswahl',
|
|
||||||
body: 'Für eine abonnierte Mannschaft steht ein Punktspiel an.',
|
|
||||||
bodyForUser: (user, settings) => matchSummary(selectedMatchesForUser(user, settings, context.allMatches), 'Für eine abonnierte Mannschaft steht ein Punktspiel an.'),
|
|
||||||
data: { type: 'team_matches', date: dateKey, season },
|
|
||||||
predicate: (user, settings) => settings.notificationTime === time &&
|
|
||||||
notificationSeasonForSettings(settings, defaultSeason) === season &&
|
|
||||||
settings.allTeamMatches === false &&
|
|
||||||
selectedMatchesForUser(user, settings, context.allMatches).length > 0,
|
|
||||||
failureLabel: 'FCM Mannschaftsauswahl-Push'
|
|
||||||
})))
|
|
||||||
|
|
||||||
ownResults.push(await sendIfDue(state, dateKey, time, 'ownTeamMatches:' + season, context.allMatches.length > 0, () => sendPushToUsers({
|
|
||||||
title: 'Punktspiel deiner Mannschaft',
|
|
||||||
body: 'Für deine Mannschaft steht ein Punktspiel an.',
|
|
||||||
bodyForUser: (user, settings) => matchSummary(ownMatchesForUser(user, settings, context.allMatches, context.teamRows), 'Für deine Mannschaft steht ein Punktspiel an.'),
|
|
||||||
data: { type: 'team_matches', date: dateKey, season },
|
|
||||||
predicate: (user, settings) => settings.notificationTime === time &&
|
|
||||||
notificationSeasonForSettings(settings, defaultSeason) === season &&
|
|
||||||
settings.allTeamMatches === false &&
|
|
||||||
selectedMatchesForUser(user, settings, context.allMatches).length === 0 &&
|
|
||||||
ownMatchesForUser(user, settings, context.allMatches, context.teamRows).length > 0,
|
|
||||||
failureLabel: 'FCM eigene-Mannschaft-Push'
|
|
||||||
})))
|
|
||||||
}
|
}
|
||||||
results.allTeamMatches = allResults.some(Boolean)
|
results.teamMatches = teamMatchResults.some(Boolean)
|
||||||
results.selectedTeamMatches = selectedResults.some(Boolean)
|
results.allTeamMatches = results.teamMatches
|
||||||
results.ownTeamMatches = ownResults.some(Boolean)
|
results.selectedTeamMatches = results.teamMatches
|
||||||
|
results.ownTeamMatches = results.teamMatches
|
||||||
|
|
||||||
results.birthdays = await sendIfDue(state, dateKey, time, 'birthdays', todaysBirthdays.length > 0, () => sendPushToUsers({
|
results.birthdays = await sendIfDue(state, dateKey, time, 'birthdays', todaysBirthdays.length > 0, () => sendPushToUsers({
|
||||||
title: 'Geburtstage heute',
|
title: 'Geburtstage heute',
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ vi.mock('../server/utils/logger.js', () => ({
|
|||||||
const authUtils = await import('../server/utils/auth.js')
|
const authUtils = await import('../server/utils/auth.js')
|
||||||
const memberUtils = await import('../server/utils/members.js')
|
const memberUtils = await import('../server/utils/members.js')
|
||||||
const pushUtils = await import('../server/utils/push-notifications.js')
|
const pushUtils = await import('../server/utils/push-notifications.js')
|
||||||
|
const spielplanUtils = await import('../server/utils/spielplan-data.js')
|
||||||
|
|
||||||
const { runNotificationSchedulerTick } = await import('../server/utils/notification-scheduler.js')
|
const { runNotificationSchedulerTick } = await import('../server/utils/notification-scheduler.js')
|
||||||
|
|
||||||
@@ -53,11 +54,22 @@ const recipient = {
|
|||||||
describe('Notification Scheduler', () => {
|
describe('Notification Scheduler', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
vi.spyOn(fs, 'readFile').mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }))
|
vi.spyOn(fs, 'readFile').mockImplementation(async (filePath) => {
|
||||||
|
if (String(filePath).includes('mannschaften_25--26.csv')) {
|
||||||
|
return [
|
||||||
|
'Mannschaft,Liga,Staffelleiter,Telefon,Heimspieltag,Spielsystem,Mannschaftsführer,Spieler,Weitere Informationen Link,Letzte Aktualisierung',
|
||||||
|
'Erwachsene 1,,,,,,Mannschaftsfuehrer,Max Spieler,,',
|
||||||
|
'Erwachsene 2,,,,,,Andere Person,Andere Spieler,,'
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
|
||||||
|
})
|
||||||
vi.spyOn(fs, 'mkdir').mockResolvedValue(undefined)
|
vi.spyOn(fs, 'mkdir').mockResolvedValue(undefined)
|
||||||
vi.spyOn(fs, 'writeFile').mockResolvedValue(undefined)
|
vi.spyOn(fs, 'writeFile').mockResolvedValue(undefined)
|
||||||
memberUtils.readMembers.mockResolvedValue([])
|
memberUtils.readMembers.mockResolvedValue([])
|
||||||
authUtils.readUsers.mockResolvedValue([recipient])
|
authUtils.readUsers.mockResolvedValue([recipient])
|
||||||
|
spielplanUtils.getDefaultSpielplanSeason.mockResolvedValue('25--26')
|
||||||
|
spielplanUtils.readSpielplanData.mockResolvedValue({ data: [] })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sendet Geburtstags-Push nur fuer Mitglieder mit expliziter Geburtstagsfreigabe', async () => {
|
it('sendet Geburtstags-Push nur fuer Mitglieder mit expliziter Geburtstagsfreigabe', async () => {
|
||||||
@@ -90,4 +102,90 @@ describe('Notification Scheduler', () => {
|
|||||||
expect(payload.body).not.toMatch(/\b\d+\b/)
|
expect(payload.body).not.toMatch(/\b\d+\b/)
|
||||||
expect(payload.body).not.toContain('Jahre')
|
expect(payload.body).not.toContain('Jahre')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('sendet Punktspiel-Push nur einmal, wenn alle, eigene und ausgewaehlte Mannschaft dasselbe Spiel treffen', async () => {
|
||||||
|
const matchUser = {
|
||||||
|
id: 'match-user',
|
||||||
|
name: 'Max Spieler',
|
||||||
|
active: true,
|
||||||
|
notificationSettings: {
|
||||||
|
allTeamMatches: true,
|
||||||
|
ownTeamMatches: true,
|
||||||
|
selectedTeamSlugs: ['erwachsene-1'],
|
||||||
|
selectedTeamSeason: '25--26',
|
||||||
|
notificationTime: '09:00'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
authUtils.readUsers.mockResolvedValue([matchUser])
|
||||||
|
spielplanUtils.readSpielplanData.mockResolvedValue({
|
||||||
|
data: [{
|
||||||
|
Termin: '14.06.2026 20:15',
|
||||||
|
BegegnungNr: 'spiel-1',
|
||||||
|
Altersklasse: 'Erwachsene',
|
||||||
|
HeimVereinName: 'Harheimer TC',
|
||||||
|
HeimMannschaftAltersklasse: 'Erwachsene',
|
||||||
|
HeimMannschaftNr: '1',
|
||||||
|
HeimMannschaft: 'Harheimer TC',
|
||||||
|
GastVereinName: 'Gastverein',
|
||||||
|
GastMannschaftAltersklasse: 'Erwachsene',
|
||||||
|
GastMannschaftNr: '1',
|
||||||
|
GastMannschaft: 'Gastverein'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
await runNotificationSchedulerTick(schedulerNow)
|
||||||
|
|
||||||
|
expect(pushUtils.sendPushToUsers).toHaveBeenCalledTimes(1)
|
||||||
|
const payload = pushUtils.sendPushToUsers.mock.calls[0][0]
|
||||||
|
expect(payload.title).toBe('Punktspiele')
|
||||||
|
expect(payload.predicate(matchUser, matchUser.notificationSettings)).toBe(true)
|
||||||
|
expect(payload.bodyForUser(matchUser, matchUser.notificationSettings)).toContain('Harheimer TC - Gastverein')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fasst eigene und ausgewaehlte Punktspiele in einer Benachrichtigung zusammen', async () => {
|
||||||
|
const matchUser = {
|
||||||
|
id: 'match-user',
|
||||||
|
name: 'Max Spieler',
|
||||||
|
active: true,
|
||||||
|
notificationSettings: {
|
||||||
|
allTeamMatches: false,
|
||||||
|
ownTeamMatches: true,
|
||||||
|
selectedTeamSlugs: ['erwachsene-2'],
|
||||||
|
selectedTeamSeason: '25--26',
|
||||||
|
notificationTime: '09:00'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
authUtils.readUsers.mockResolvedValue([matchUser])
|
||||||
|
spielplanUtils.readSpielplanData.mockResolvedValue({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
Termin: '14.06.2026 20:15',
|
||||||
|
BegegnungNr: 'spiel-1',
|
||||||
|
Altersklasse: 'Erwachsene',
|
||||||
|
HeimVereinName: 'Harheimer TC',
|
||||||
|
HeimMannschaftAltersklasse: 'Erwachsene',
|
||||||
|
HeimMannschaftNr: '1',
|
||||||
|
HeimMannschaft: 'Harheimer TC',
|
||||||
|
GastMannschaft: 'Gastverein'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Termin: '14.06.2026 20:30',
|
||||||
|
BegegnungNr: 'spiel-2',
|
||||||
|
Altersklasse: 'Erwachsene',
|
||||||
|
HeimMannschaft: 'Gastverein II',
|
||||||
|
GastVereinName: 'Harheimer TC',
|
||||||
|
GastMannschaftAltersklasse: 'Erwachsene',
|
||||||
|
GastMannschaftNr: '2',
|
||||||
|
GastMannschaft: 'Harheimer TC II'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
await runNotificationSchedulerTick(schedulerNow)
|
||||||
|
|
||||||
|
expect(pushUtils.sendPushToUsers).toHaveBeenCalledTimes(1)
|
||||||
|
const payload = pushUtils.sendPushToUsers.mock.calls[0][0]
|
||||||
|
expect(payload.predicate(matchUser, matchUser.notificationSettings)).toBe(true)
|
||||||
|
expect(payload.bodyForUser(matchUser, matchUser.notificationSettings)).toBe('2 Punktspiele am 14.06.2026')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user