Compare commits
10 Commits
af41531597
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ce8057c6b | ||
|
|
5415247f47 | ||
|
|
f57ea205c7 | ||
|
|
4fc41a5b2c | ||
|
|
4c0f227e27 | ||
|
|
ddba4eca37 | ||
|
|
411bac8162 | ||
|
|
4496cd97f9 | ||
|
|
5c9b2b9d8f | ||
|
|
a1d631037e |
20
package.json
20
package.json
@@ -43,8 +43,8 @@
|
||||
"emoji-regex": "^10.5.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"mermaid": "^11.12.0",
|
||||
"next": "15.5.3",
|
||||
"next-auth": "^4.24.11",
|
||||
"next": "15.5.7",
|
||||
"next-auth": "^4.24.12",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"prisma": "^6.16.1",
|
||||
"react": "19.1.0",
|
||||
@@ -60,24 +60,30 @@
|
||||
"twemoji": "^14.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "^15.5.3",
|
||||
"eslint-config-next": "^15.5.7",
|
||||
"husky": "^9.1.7",
|
||||
"knip": "^5.64.0",
|
||||
"knip": "^5.71.0",
|
||||
"lint-staged": "^15.5.2",
|
||||
"prettier": "^3.6.2",
|
||||
"sharp": "^0.34.5",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"esbuild": ">=0.25.0",
|
||||
"mdast-util-to-hast": ">=13.2.1"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,json,css,md}": [
|
||||
"prettier --write"
|
||||
|
||||
1107
pnpm-lock.yaml
generated
1107
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { ManagerSummary } from '@/services/analytics/manager-summary';
|
||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Tabs, TabItem } from '@/components/ui/Tabs';
|
||||
import { AchievementCard } from '@/components/ui/AchievementCard';
|
||||
import { ChallengeCard } from '@/components/ui/ChallengeCard';
|
||||
import { TaskCard } from '@/components/ui/TaskCard';
|
||||
import { FilterChip } from '@/components/ui/FilterChip';
|
||||
import { useTasksContext } from '@/contexts/TasksContext';
|
||||
import { MetricsTab } from './MetricsTab';
|
||||
import { format } from 'date-fns';
|
||||
@@ -22,15 +22,19 @@ export default function ManagerWeeklySummary({
|
||||
initialSummary,
|
||||
}: ManagerWeeklySummaryProps) {
|
||||
const [summary] = useState<ManagerSummary>(initialSummary);
|
||||
const [activeView, setActiveView] = useState<
|
||||
'narrative' | 'accomplishments' | 'challenges' | 'metrics'
|
||||
>('narrative');
|
||||
const [activeView, setActiveView] = useState<'narrative' | 'metrics'>(
|
||||
'narrative'
|
||||
);
|
||||
const [selectedAccomplishmentTags, setSelectedAccomplishmentTags] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
const [selectedChallengeTags, setSelectedChallengeTags] = useState<string[]>(
|
||||
[]
|
||||
);
|
||||
const { tags: availableTags } = useTasksContext();
|
||||
|
||||
const handleTabChange = (tabId: string) => {
|
||||
setActiveView(
|
||||
tabId as 'narrative' | 'accomplishments' | 'challenges' | 'metrics'
|
||||
);
|
||||
setActiveView(tabId as 'narrative' | 'metrics');
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
@@ -42,21 +46,99 @@ export default function ManagerWeeklySummary({
|
||||
return `7 derniers jours (${format(summary.period.start, 'dd MMM', { locale: fr })} - ${format(summary.period.end, 'dd MMM yyyy', { locale: fr })})`;
|
||||
};
|
||||
|
||||
// Calculer les compteurs pour chaque tag basés uniquement sur les accomplishments
|
||||
const accomplishmentTagCounts = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
availableTags.forEach((tag) => {
|
||||
counts[tag.name] = summary.keyAccomplishments.filter((a) =>
|
||||
a.tags.includes(tag.name)
|
||||
).length;
|
||||
});
|
||||
return counts;
|
||||
}, [availableTags, summary.keyAccomplishments]);
|
||||
|
||||
// Calculer les compteurs pour chaque tag basés uniquement sur les challenges
|
||||
const challengeTagCounts = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
availableTags.forEach((tag) => {
|
||||
counts[tag.name] = summary.upcomingChallenges.filter((c) =>
|
||||
c.tags.includes(tag.name)
|
||||
).length;
|
||||
});
|
||||
return counts;
|
||||
}, [availableTags, summary.upcomingChallenges]);
|
||||
|
||||
// Trier les tags pour les accomplishments par nombre d'utilisation (décroissant)
|
||||
const sortedAccomplishmentTags = useMemo(() => {
|
||||
return [...availableTags]
|
||||
.filter((tag) => (accomplishmentTagCounts[tag.name] || 0) > 0)
|
||||
.sort((a, b) => {
|
||||
const countA = accomplishmentTagCounts[a.name] || 0;
|
||||
const countB = accomplishmentTagCounts[b.name] || 0;
|
||||
return countB - countA;
|
||||
});
|
||||
}, [availableTags, accomplishmentTagCounts]);
|
||||
|
||||
// Trier les tags pour les challenges par nombre d'utilisation (décroissant)
|
||||
const sortedChallengeTags = useMemo(() => {
|
||||
return [...availableTags]
|
||||
.filter((tag) => (challengeTagCounts[tag.name] || 0) > 0)
|
||||
.sort((a, b) => {
|
||||
const countA = challengeTagCounts[a.name] || 0;
|
||||
const countB = challengeTagCounts[b.name] || 0;
|
||||
return countB - countA;
|
||||
});
|
||||
}, [availableTags, challengeTagCounts]);
|
||||
|
||||
// Filtrer les accomplishments selon les tags sélectionnés
|
||||
const filteredAccomplishments = useMemo(() => {
|
||||
if (selectedAccomplishmentTags.length === 0) {
|
||||
return summary.keyAccomplishments;
|
||||
}
|
||||
return summary.keyAccomplishments.filter((accomplishment) =>
|
||||
selectedAccomplishmentTags.some((tag) =>
|
||||
accomplishment.tags.includes(tag)
|
||||
)
|
||||
);
|
||||
}, [summary.keyAccomplishments, selectedAccomplishmentTags]);
|
||||
|
||||
// Filtrer les challenges selon les tags sélectionnés
|
||||
const filteredChallenges = useMemo(() => {
|
||||
if (selectedChallengeTags.length === 0) {
|
||||
return summary.upcomingChallenges;
|
||||
}
|
||||
return summary.upcomingChallenges.filter((challenge) =>
|
||||
selectedChallengeTags.some((tag) => challenge.tags.includes(tag))
|
||||
);
|
||||
}, [summary.upcomingChallenges, selectedChallengeTags]);
|
||||
|
||||
const handleAccomplishmentTagToggle = (tagName: string) => {
|
||||
setSelectedAccomplishmentTags((prev) =>
|
||||
prev.includes(tagName)
|
||||
? prev.filter((t) => t !== tagName)
|
||||
: [...prev, tagName]
|
||||
);
|
||||
};
|
||||
|
||||
const handleChallengeTagToggle = (tagName: string) => {
|
||||
setSelectedChallengeTags((prev) =>
|
||||
prev.includes(tagName)
|
||||
? prev.filter((t) => t !== tagName)
|
||||
: [...prev, tagName]
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearAccomplishmentFilters = () => {
|
||||
setSelectedAccomplishmentTags([]);
|
||||
};
|
||||
|
||||
const handleClearChallengeFilters = () => {
|
||||
setSelectedChallengeTags([]);
|
||||
};
|
||||
|
||||
// Configuration des onglets
|
||||
const tabItems: TabItem[] = [
|
||||
{ id: 'narrative', label: 'Vue Executive', icon: '📝' },
|
||||
{
|
||||
id: 'accomplishments',
|
||||
label: 'Accomplissements',
|
||||
icon: '✅',
|
||||
count: summary.keyAccomplishments.length,
|
||||
},
|
||||
{
|
||||
id: 'challenges',
|
||||
label: 'Enjeux à venir',
|
||||
icon: '🎯',
|
||||
count: summary.upcomingChallenges.length,
|
||||
},
|
||||
{ id: 'metrics', label: 'Métriques', icon: '📊' },
|
||||
];
|
||||
|
||||
@@ -197,7 +279,7 @@ export default function ManagerWeeklySummary({
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Top accomplissements */}
|
||||
{/* Accomplissements détaillés */}
|
||||
<Card variant="elevated">
|
||||
<CardHeader
|
||||
className="pb-4"
|
||||
@@ -211,42 +293,106 @@ export default function ManagerWeeklySummary({
|
||||
className="text-lg font-semibold flex items-center gap-2"
|
||||
style={{ color: 'var(--success)' }}
|
||||
>
|
||||
<Emoji emoji="🏆" /> Top accomplissements
|
||||
<Emoji emoji="🏆" /> Accomplissements
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mt-1">
|
||||
{filteredAccomplishments.length} accomplissement
|
||||
{filteredAccomplishments.length > 1 ? 's' : ''} affiché
|
||||
{selectedAccomplishmentTags.length > 0 && (
|
||||
<span className="ml-2">
|
||||
(sur {summary.keyAccomplishments.length} total)
|
||||
</span>
|
||||
)}
|
||||
{selectedAccomplishmentTags.length === 0 && (
|
||||
<>
|
||||
{' '}
|
||||
• {summary.metrics.totalTasksCompleted} tâches •{' '}
|
||||
{summary.metrics.totalCheckboxesCompleted} todos complétés
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</CardHeader>
|
||||
{/* Filtres par tags pour les accomplishments */}
|
||||
{sortedAccomplishmentTags.length > 0 && (
|
||||
<CardContent className="pb-4 border-b border-[var(--border)]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-xs font-semibold flex items-center gap-2 text-[var(--muted-foreground)]">
|
||||
<Emoji emoji="🏷️" /> Filtres
|
||||
</h3>
|
||||
{selectedAccomplishmentTags.length > 0 && (
|
||||
<Button
|
||||
onClick={handleClearAccomplishmentFilters}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-xs h-6"
|
||||
>
|
||||
Réinitialiser ({selectedAccomplishmentTags.length})
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{sortedAccomplishmentTags.map((tag) => (
|
||||
<FilterChip
|
||||
key={tag.id}
|
||||
onClick={() => handleAccomplishmentTagToggle(tag.name)}
|
||||
variant={
|
||||
selectedAccomplishmentTags.includes(tag.name)
|
||||
? 'selected'
|
||||
: 'tag'
|
||||
}
|
||||
color={tag.color}
|
||||
count={accomplishmentTagCounts[tag.name]}
|
||||
>
|
||||
{tag.name}
|
||||
</FilterChip>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{summary.keyAccomplishments.length === 0 ? (
|
||||
{filteredAccomplishments.length === 0 ? (
|
||||
<div className="col-span-3 text-center py-8 text-[var(--muted-foreground)]">
|
||||
<p>
|
||||
<div className="text-4xl mb-4">
|
||||
<Emoji emoji="📭" />
|
||||
</div>
|
||||
<p className="text-lg mb-2">
|
||||
Aucun accomplissement significatif trouvé cette semaine.
|
||||
</p>
|
||||
<p className="text-sm mt-2">
|
||||
<p className="text-sm">
|
||||
Ajoutez des tâches avec priorité haute/medium ou des
|
||||
meetings.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
summary.keyAccomplishments
|
||||
.slice(0, 6)
|
||||
.map((accomplishment, index) => (
|
||||
<AchievementCard
|
||||
key={accomplishment.id}
|
||||
achievement={accomplishment}
|
||||
availableTags={
|
||||
availableTags as (Tag & { usage: number })[]
|
||||
}
|
||||
index={index}
|
||||
showDescription={true}
|
||||
maxTags={2}
|
||||
/>
|
||||
))
|
||||
filteredAccomplishments.map((accomplishment) => (
|
||||
<TaskCard
|
||||
key={accomplishment.id}
|
||||
variant="detailed"
|
||||
title={accomplishment.title}
|
||||
description={accomplishment.description}
|
||||
tags={accomplishment.tags}
|
||||
priority={
|
||||
accomplishment.impact === 'high'
|
||||
? 'high'
|
||||
: accomplishment.impact === 'medium'
|
||||
? 'medium'
|
||||
: 'low'
|
||||
}
|
||||
status="done"
|
||||
completedAt={accomplishment.completedAt}
|
||||
todosCount={accomplishment.todosCount}
|
||||
availableTags={availableTags as Tag[]}
|
||||
source="manual"
|
||||
style={{ opacity: 1 }}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Top challenges */}
|
||||
{/* Enjeux à venir détaillés */}
|
||||
<Card variant="elevated">
|
||||
<CardHeader
|
||||
className="pb-4"
|
||||
@@ -260,34 +406,106 @@ export default function ManagerWeeklySummary({
|
||||
className="text-lg font-semibold flex items-center gap-2"
|
||||
style={{ color: 'var(--destructive)' }}
|
||||
>
|
||||
<Emoji emoji="🎯" /> Top enjeux à venir
|
||||
<Emoji emoji="🎯" /> Enjeux à venir
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mt-1">
|
||||
{filteredChallenges.length} défi
|
||||
{filteredChallenges.length > 1 ? 's' : ''} affiché
|
||||
{selectedChallengeTags.length > 0 && (
|
||||
<span className="ml-2">
|
||||
(sur {summary.upcomingChallenges.length} total)
|
||||
</span>
|
||||
)}
|
||||
{selectedChallengeTags.length === 0 && (
|
||||
<>
|
||||
{' '}
|
||||
•{' '}
|
||||
{
|
||||
summary.upcomingChallenges.filter(
|
||||
(c) => c.priority === 'high'
|
||||
).length
|
||||
}{' '}
|
||||
priorité haute •{' '}
|
||||
{
|
||||
summary.upcomingChallenges.filter(
|
||||
(c) => c.blockers.length > 0
|
||||
).length
|
||||
}{' '}
|
||||
avec blockers
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* Filtres par tags pour les challenges */}
|
||||
{sortedChallengeTags.length > 0 && (
|
||||
<CardContent className="pb-4 border-b border-[var(--border)]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-xs font-semibold flex items-center gap-2 text-[var(--muted-foreground)]">
|
||||
<Emoji emoji="🏷️" /> Filtres
|
||||
</h3>
|
||||
{selectedChallengeTags.length > 0 && (
|
||||
<Button
|
||||
onClick={handleClearChallengeFilters}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-xs h-6"
|
||||
>
|
||||
Réinitialiser ({selectedChallengeTags.length})
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{sortedChallengeTags.map((tag) => (
|
||||
<FilterChip
|
||||
key={tag.id}
|
||||
onClick={() => handleChallengeTagToggle(tag.name)}
|
||||
variant={
|
||||
selectedChallengeTags.includes(tag.name)
|
||||
? 'selected'
|
||||
: 'tag'
|
||||
}
|
||||
color={tag.color}
|
||||
count={challengeTagCounts[tag.name]}
|
||||
>
|
||||
{tag.name}
|
||||
</FilterChip>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
<CardContent
|
||||
className={sortedChallengeTags.length > 0 ? 'pt-4' : ''}
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{summary.upcomingChallenges.length === 0 ? (
|
||||
{filteredChallenges.length === 0 ? (
|
||||
<div className="col-span-3 text-center py-8 text-[var(--muted-foreground)]">
|
||||
<p>Aucun enjeu prioritaire trouvé.</p>
|
||||
<p className="text-sm mt-2">
|
||||
<div className="text-4xl mb-4">
|
||||
<Emoji emoji="🎯" />
|
||||
</div>
|
||||
<p className="text-lg mb-2">
|
||||
Aucun enjeu prioritaire trouvé.
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
Ajoutez des tâches non complétées avec priorité
|
||||
haute/medium.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
summary.upcomingChallenges
|
||||
.slice(0, 6)
|
||||
.map((challenge, index) => (
|
||||
<ChallengeCard
|
||||
key={challenge.id}
|
||||
challenge={challenge}
|
||||
availableTags={
|
||||
availableTags as (Tag & { usage: number })[]
|
||||
}
|
||||
index={index}
|
||||
showDescription={true}
|
||||
maxTags={2}
|
||||
/>
|
||||
))
|
||||
filteredChallenges.map((challenge) => (
|
||||
<TaskCard
|
||||
key={challenge.id}
|
||||
variant="detailed"
|
||||
title={challenge.title}
|
||||
description={challenge.description}
|
||||
tags={challenge.tags}
|
||||
priority={challenge.priority}
|
||||
status="todo"
|
||||
dueDate={challenge.deadline}
|
||||
todosCount={challenge.todosCount}
|
||||
availableTags={availableTags as Tag[]}
|
||||
source="manual"
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -295,118 +513,6 @@ export default function ManagerWeeklySummary({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Vue détaillée des accomplissements */}
|
||||
{activeView === 'accomplishments' && (
|
||||
<Card variant="elevated">
|
||||
<CardHeader>
|
||||
<h2 className="text-lg font-semibold">
|
||||
<Emoji emoji="✅" /> Accomplissements des 7 derniers jours
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
{summary.keyAccomplishments.length} accomplissements significatifs
|
||||
• {summary.metrics.totalTasksCompleted} tâches •{' '}
|
||||
{summary.metrics.totalCheckboxesCompleted} todos complétés
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{summary.keyAccomplishments.length === 0 ? (
|
||||
<div
|
||||
className="p-8 text-center rounded-xl border-2"
|
||||
style={{
|
||||
backgroundColor:
|
||||
'color-mix(in srgb, var(--muted) 15%, transparent)',
|
||||
borderColor:
|
||||
'color-mix(in srgb, var(--muted) 40%, var(--border))',
|
||||
color: 'var(--muted-foreground)',
|
||||
}}
|
||||
>
|
||||
<div className="text-4xl mb-4">
|
||||
<Emoji emoji="📭" />
|
||||
</div>
|
||||
<p className="text-lg mb-2">
|
||||
Aucun accomplissement significatif trouvé cette semaine.
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
Ajoutez des tâches avec priorité haute/medium ou des meetings.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{summary.keyAccomplishments.map((accomplishment, index) => (
|
||||
<AchievementCard
|
||||
key={accomplishment.id}
|
||||
achievement={accomplishment}
|
||||
availableTags={availableTags as (Tag & { usage: number })[]}
|
||||
index={index}
|
||||
showDescription={true}
|
||||
maxTags={3}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Vue détaillée des challenges */}
|
||||
{activeView === 'challenges' && (
|
||||
<Card variant="elevated">
|
||||
<CardHeader>
|
||||
<h2 className="text-lg font-semibold">
|
||||
<Emoji emoji="🎯" /> Enjeux et défis à venir
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
{summary.upcomingChallenges.length} défis identifiés •{' '}
|
||||
{
|
||||
summary.upcomingChallenges.filter((c) => c.priority === 'high')
|
||||
.length
|
||||
}{' '}
|
||||
priorité haute •{' '}
|
||||
{
|
||||
summary.upcomingChallenges.filter((c) => c.blockers.length > 0)
|
||||
.length
|
||||
}{' '}
|
||||
avec blockers
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{summary.upcomingChallenges.length === 0 ? (
|
||||
<div
|
||||
className="p-8 text-center rounded-xl border-2"
|
||||
style={{
|
||||
backgroundColor:
|
||||
'color-mix(in srgb, var(--muted) 15%, transparent)',
|
||||
borderColor:
|
||||
'color-mix(in srgb, var(--muted) 40%, var(--border))',
|
||||
color: 'var(--muted-foreground)',
|
||||
}}
|
||||
>
|
||||
<div className="text-4xl mb-4">
|
||||
<Emoji emoji="🎯" />
|
||||
</div>
|
||||
<p className="text-lg mb-2">Aucun enjeu prioritaire trouvé.</p>
|
||||
<p className="text-sm">
|
||||
Ajoutez des tâches non complétées avec priorité haute/medium.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{summary.upcomingChallenges.map((challenge, index) => (
|
||||
<ChallengeCard
|
||||
key={challenge.id}
|
||||
challenge={challenge}
|
||||
availableTags={availableTags as (Tag & { usage: number })[]}
|
||||
index={index}
|
||||
showDescription={true}
|
||||
maxTags={3}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Vue Métriques */}
|
||||
{activeView === 'metrics' && <MetricsTab />}
|
||||
</div>
|
||||
|
||||
@@ -6,58 +6,9 @@ import { StatCard } from '@/components/ui/StatCard';
|
||||
import { ActionCard } from '@/components/ui/ActionCard';
|
||||
import { TaskCard } from '@/components/ui/TaskCard';
|
||||
import { MetricCard } from '@/components/ui/MetricCard';
|
||||
import { AchievementCard } from '@/components/ui/AchievementCard';
|
||||
import { ChallengeCard } from '@/components/ui/ChallengeCard';
|
||||
import { SkeletonCard } from '@/components/ui/SkeletonCard';
|
||||
import { AchievementData } from '@/components/ui/AchievementCard';
|
||||
import { ChallengeData } from '@/components/ui/ChallengeCard';
|
||||
|
||||
export function CardsSection() {
|
||||
const sampleAchievements: AchievementData[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Refactoring de la page Daily',
|
||||
description: 'Migration vers les composants UI génériques',
|
||||
impact: 'high',
|
||||
completedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
tags: ['refactoring', 'ui'],
|
||||
todosCount: 8,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Implémentation du système de thèmes',
|
||||
description: 'Ajout de 10 nouveaux thèmes avec CSS variables',
|
||||
impact: 'medium',
|
||||
completedAt: new Date(Date.now() - 86400000),
|
||||
updatedAt: new Date(Date.now() - 86400000),
|
||||
tags: ['themes', 'css'],
|
||||
todosCount: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const sampleChallenges: ChallengeData[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Migration vers Next.js 15',
|
||||
description: 'Mise à jour majeure avec nouvelles fonctionnalités',
|
||||
priority: 'high',
|
||||
deadline: new Date(Date.now() + 7 * 86400000),
|
||||
tags: ['migration', 'nextjs'],
|
||||
todosCount: 12,
|
||||
blockers: ['Tests à mettre à jour'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Optimisation des performances',
|
||||
description: 'Réduction du temps de chargement',
|
||||
priority: 'medium',
|
||||
deadline: new Date(Date.now() + 14 * 86400000),
|
||||
tags: ['performance', 'optimization'],
|
||||
todosCount: 5,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section id="cards" className="space-y-8">
|
||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||
@@ -154,40 +105,6 @@ export function CardsSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Achievement Cards */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-[var(--foreground)]">
|
||||
Achievement Cards
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{sampleAchievements.map((achievement, index) => (
|
||||
<AchievementCard
|
||||
key={achievement.id}
|
||||
achievement={achievement}
|
||||
availableTags={[]}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Challenge Cards */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-[var(--foreground)]">
|
||||
Challenge Cards
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{sampleChallenges.map((challenge, index) => (
|
||||
<ChallengeCard
|
||||
key={challenge.id}
|
||||
challenge={challenge}
|
||||
availableTags={[]}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metric Cards */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-[var(--foreground)]">
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { format } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { TagDisplay } from '@/components/ui/TagDisplay';
|
||||
import { PriorityBadge } from '@/components/ui/PriorityBadge';
|
||||
import { Tag } from '@/lib/types';
|
||||
import { Emoji } from '@/components/ui/Emoji';
|
||||
|
||||
export interface AchievementData {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
impact: 'low' | 'medium' | 'high';
|
||||
completedAt: Date;
|
||||
updatedAt: Date;
|
||||
tags?: string[];
|
||||
todosCount?: number;
|
||||
}
|
||||
|
||||
interface AchievementCardProps {
|
||||
achievement: AchievementData;
|
||||
availableTags: (Tag & { usage: number })[];
|
||||
index: number;
|
||||
showDescription?: boolean;
|
||||
maxTags?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AchievementCard({
|
||||
achievement,
|
||||
availableTags,
|
||||
index,
|
||||
showDescription = true,
|
||||
maxTags = 2,
|
||||
className = '',
|
||||
}: AchievementCardProps) {
|
||||
// Détecter si c'est un todo (ID commence par "todo-")
|
||||
const isTodo = achievement.id.startsWith('todo-');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group ${className}`}
|
||||
style={{
|
||||
backgroundColor: isTodo
|
||||
? 'color-mix(in srgb, var(--accent) 8%, var(--card))'
|
||||
: 'color-mix(in srgb, var(--success) 5%, var(--card))',
|
||||
}}
|
||||
>
|
||||
{/* Barre colorée gauche */}
|
||||
<div className="absolute left-0 top-0 bottom-0 w-1 bg-green-500 rounded-l-lg"></div>
|
||||
|
||||
{/* Header compact */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-5 h-5 rounded-full text-xs font-bold flex items-center justify-center text-[var(--success)] bg-[var(--success)]/15 border border-[var(--success)]/25">
|
||||
#{index + 1}
|
||||
</span>
|
||||
<PriorityBadge priority={achievement.impact} />
|
||||
</div>
|
||||
<div className="text-xs text-[var(--muted-foreground)] text-right">
|
||||
<div>
|
||||
Terminé: {format(achievement.completedAt, 'dd/MM', { locale: fr })}
|
||||
</div>
|
||||
{achievement.updatedAt &&
|
||||
achievement.updatedAt.getTime() !==
|
||||
achievement.completedAt.getTime() && (
|
||||
<div>
|
||||
Mis à jour:{' '}
|
||||
{format(achievement.updatedAt, 'dd/MM', { locale: fr })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Titre */}
|
||||
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
|
||||
{achievement.title}
|
||||
</h4>
|
||||
|
||||
{/* Tags */}
|
||||
{achievement.tags && achievement.tags.length > 0 && (
|
||||
<div className="mb-2">
|
||||
<TagDisplay
|
||||
tags={achievement.tags}
|
||||
availableTags={availableTags as Tag[]}
|
||||
size="sm"
|
||||
maxTags={maxTags}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description si disponible */}
|
||||
{showDescription && achievement.description && (
|
||||
<p className="text-xs text-[var(--muted-foreground)] line-clamp-2 leading-relaxed mb-2">
|
||||
{achievement.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Count de todos - seulement pour les tâches, pas pour les todos standalone */}
|
||||
{!isTodo &&
|
||||
achievement.todosCount !== undefined &&
|
||||
achievement.todosCount > 0 && (
|
||||
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
|
||||
<span>
|
||||
<Emoji emoji="📋" />
|
||||
</span>
|
||||
<span>
|
||||
{achievement.todosCount} todo
|
||||
{achievement.todosCount > 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { format } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { TagDisplay } from '@/components/ui/TagDisplay';
|
||||
import { PriorityBadge } from '@/components/ui/PriorityBadge';
|
||||
import { Tag } from '@/lib/types';
|
||||
import { Emoji } from '@/components/ui/Emoji';
|
||||
|
||||
export interface ChallengeData {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
deadline?: Date;
|
||||
tags?: string[];
|
||||
todosCount?: number;
|
||||
blockers?: string[];
|
||||
}
|
||||
|
||||
interface ChallengeCardProps {
|
||||
challenge: ChallengeData;
|
||||
availableTags: (Tag & { usage: number })[];
|
||||
index: number;
|
||||
showDescription?: boolean;
|
||||
maxTags?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ChallengeCard({
|
||||
challenge,
|
||||
availableTags,
|
||||
index,
|
||||
showDescription = true,
|
||||
maxTags = 2,
|
||||
className = '',
|
||||
}: ChallengeCardProps) {
|
||||
return (
|
||||
<div
|
||||
className={`relative border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group ${className}`}
|
||||
style={{
|
||||
backgroundColor:
|
||||
'color-mix(in srgb, var(--destructive) 5%, var(--card))',
|
||||
}}
|
||||
>
|
||||
{/* Barre colorée gauche */}
|
||||
<div className="absolute left-0 top-0 bottom-0 w-1 bg-orange-500 rounded-l-lg"></div>
|
||||
|
||||
{/* Header compact */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-5 h-5 rounded-full text-xs font-bold flex items-center justify-center text-[var(--accent)] bg-[var(--accent)]/15 border border-[var(--accent)]/25">
|
||||
#{index + 1}
|
||||
</span>
|
||||
<PriorityBadge priority={challenge.priority} />
|
||||
</div>
|
||||
{challenge.deadline && (
|
||||
<span className="text-xs text-[var(--muted-foreground)]">
|
||||
{format(challenge.deadline, 'dd/MM', { locale: fr })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Titre */}
|
||||
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
|
||||
{challenge.title}
|
||||
</h4>
|
||||
|
||||
{/* Tags */}
|
||||
{challenge.tags && challenge.tags.length > 0 && (
|
||||
<div className="mb-2">
|
||||
<TagDisplay
|
||||
tags={challenge.tags}
|
||||
availableTags={availableTags as Tag[]}
|
||||
size="sm"
|
||||
maxTags={maxTags}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description si disponible */}
|
||||
{showDescription && challenge.description && (
|
||||
<p className="text-xs text-[var(--muted-foreground)] line-clamp-2 leading-relaxed mb-2">
|
||||
{challenge.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Count de todos */}
|
||||
{challenge.todosCount !== undefined && challenge.todosCount > 0 && (
|
||||
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
|
||||
<span>
|
||||
<Emoji emoji="📋" />
|
||||
</span>
|
||||
<span>
|
||||
{challenge.todosCount} todo{challenge.todosCount > 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -27,8 +27,6 @@ export { DropZone } from './DropZone';
|
||||
export { Tabs } from './Tabs';
|
||||
export { PriorityBadge } from './PriorityBadge';
|
||||
export { StatusBadge } from './StatusBadge';
|
||||
export { AchievementCard } from './AchievementCard';
|
||||
export { ChallengeCard } from './ChallengeCard';
|
||||
|
||||
// Composants Daily
|
||||
export { CheckboxItem } from './CheckboxItem';
|
||||
|
||||
345
src/services/__tests__/notes.test.ts
Normal file
345
src/services/__tests__/notes.test.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* Tests unitaires pour NotesService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { NotesService } from '../notes';
|
||||
import { prisma } from '@/services/core/database';
|
||||
import { tagsService } from '../task-management/tags';
|
||||
|
||||
// Mock de prisma
|
||||
vi.mock('@/services/core/database', () => ({
|
||||
prisma: {
|
||||
note: {
|
||||
findMany: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
count: vi.fn(),
|
||||
},
|
||||
noteTag: {
|
||||
createMany: vi.fn(),
|
||||
deleteMany: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock de tagsService
|
||||
vi.mock('../task-management/tags', () => ({
|
||||
tagsService: {
|
||||
ensureTagsExist: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('NotesService', () => {
|
||||
let service: NotesService;
|
||||
const mockUserId = 'user-123';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
service = new NotesService();
|
||||
});
|
||||
|
||||
describe('getNotes', () => {
|
||||
it('devrait récupérer toutes les notes pour un utilisateur', async () => {
|
||||
const mockNotes = [
|
||||
{
|
||||
id: 'note-1',
|
||||
title: 'Note 1',
|
||||
content: 'Content 1',
|
||||
userId: mockUserId,
|
||||
taskId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
noteTags: [],
|
||||
task: null,
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.note.findMany).mockResolvedValue(mockNotes as any);
|
||||
|
||||
const notes = await service.getNotes(mockUserId);
|
||||
|
||||
expect(notes).toHaveLength(1);
|
||||
expect(prisma.note.findMany).toHaveBeenCalledWith({
|
||||
where: { userId: mockUserId },
|
||||
include: {
|
||||
noteTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
task: {
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
primaryTag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNoteById', () => {
|
||||
it('devrait récupérer une note par son ID', async () => {
|
||||
const mockNote = {
|
||||
id: 'note-1',
|
||||
title: 'Note 1',
|
||||
content: 'Content 1',
|
||||
userId: mockUserId,
|
||||
taskId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
noteTags: [],
|
||||
task: null,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.note.findFirst).mockResolvedValue(mockNote as any);
|
||||
|
||||
const note = await service.getNoteById('note-1', mockUserId);
|
||||
|
||||
expect(note).toBeDefined();
|
||||
expect(note?.id).toBe('note-1');
|
||||
expect(prisma.note.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: 'note-1',
|
||||
userId: mockUserId,
|
||||
},
|
||||
include: {
|
||||
noteTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
task: {
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
primaryTag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("devrait retourner null si la note n'existe pas", async () => {
|
||||
vi.mocked(prisma.note.findFirst).mockResolvedValue(null);
|
||||
|
||||
const note = await service.getNoteById('non-existent', mockUserId);
|
||||
|
||||
expect(note).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNote', () => {
|
||||
it('devrait créer une nouvelle note', async () => {
|
||||
const mockNote = {
|
||||
id: 'note-1',
|
||||
title: 'New Note',
|
||||
content: 'Content',
|
||||
userId: mockUserId,
|
||||
taskId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
noteTags: [],
|
||||
task: null,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.note.create).mockResolvedValue(mockNote as any);
|
||||
vi.mocked(prisma.note.findUnique).mockResolvedValue(mockNote as any);
|
||||
vi.mocked(tagsService.ensureTagsExist).mockResolvedValue([]);
|
||||
|
||||
const note = await service.createNote({
|
||||
title: 'New Note',
|
||||
content: 'Content',
|
||||
userId: mockUserId,
|
||||
});
|
||||
|
||||
expect(note).toBeDefined();
|
||||
expect(prisma.note.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait créer une note avec des tags', async () => {
|
||||
const mockNote = {
|
||||
id: 'note-1',
|
||||
title: 'New Note',
|
||||
content: 'Content',
|
||||
userId: mockUserId,
|
||||
taskId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
noteTags: [],
|
||||
task: null,
|
||||
};
|
||||
|
||||
const mockTags = [
|
||||
{ id: 'tag-1', name: 'Tag 1', color: '#ff0000', isPinned: false },
|
||||
];
|
||||
|
||||
vi.mocked(prisma.note.create).mockResolvedValue(mockNote as any);
|
||||
vi.mocked(prisma.note.findUnique).mockResolvedValue({
|
||||
...mockNote,
|
||||
noteTags: [{ tag: mockTags[0] }],
|
||||
} as any);
|
||||
vi.mocked(tagsService.ensureTagsExist).mockResolvedValue(mockTags as any);
|
||||
vi.mocked(prisma.noteTag.createMany).mockResolvedValue({
|
||||
count: 1,
|
||||
} as any);
|
||||
|
||||
await service.createNote({
|
||||
title: 'New Note',
|
||||
content: 'Content',
|
||||
userId: mockUserId,
|
||||
tags: ['Tag 1'],
|
||||
});
|
||||
|
||||
expect(tagsService.ensureTagsExist).toHaveBeenCalledWith(
|
||||
['Tag 1'],
|
||||
mockUserId
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateNote', () => {
|
||||
it('devrait mettre à jour une note', async () => {
|
||||
const mockNote = {
|
||||
id: 'note-1',
|
||||
title: 'Updated Note',
|
||||
content: 'Updated Content',
|
||||
userId: mockUserId,
|
||||
taskId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
noteTags: [],
|
||||
task: null,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.note.findFirst).mockResolvedValue({
|
||||
id: 'note-1',
|
||||
userId: mockUserId,
|
||||
} as any);
|
||||
vi.mocked(prisma.note.update).mockResolvedValue(mockNote as any);
|
||||
vi.mocked(prisma.note.findUnique).mockResolvedValue({
|
||||
...mockNote,
|
||||
noteTags: [],
|
||||
} as any);
|
||||
vi.mocked(prisma.noteTag.deleteMany).mockResolvedValue({
|
||||
count: 0,
|
||||
} as any);
|
||||
vi.mocked(tagsService.ensureTagsExist).mockResolvedValue([]);
|
||||
|
||||
const note = await service.updateNote('note-1', mockUserId, {
|
||||
title: 'Updated Note',
|
||||
content: 'Updated Content',
|
||||
});
|
||||
|
||||
expect(note).toBeDefined();
|
||||
expect(prisma.note.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("devrait lancer une erreur si la note n'existe pas", async () => {
|
||||
vi.mocked(prisma.note.findFirst).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.updateNote('non-existent', mockUserId, {
|
||||
title: 'Updated',
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteNote', () => {
|
||||
it('devrait supprimer une note', async () => {
|
||||
vi.mocked(prisma.note.findFirst).mockResolvedValue({
|
||||
id: 'note-1',
|
||||
userId: mockUserId,
|
||||
} as any);
|
||||
vi.mocked(prisma.note.delete).mockResolvedValue({} as any);
|
||||
|
||||
await service.deleteNote('note-1', mockUserId);
|
||||
|
||||
expect(prisma.note.delete).toHaveBeenCalledWith({
|
||||
where: { id: 'note-1' },
|
||||
});
|
||||
});
|
||||
|
||||
it("devrait lancer une erreur si la note n'existe pas", async () => {
|
||||
vi.mocked(prisma.note.findFirst).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.deleteNote('non-existent', mockUserId)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('searchNotes', () => {
|
||||
it('devrait rechercher dans les notes', async () => {
|
||||
const mockNotes = [
|
||||
{
|
||||
id: 'note-1',
|
||||
title: 'Test Note',
|
||||
content: 'Content',
|
||||
userId: mockUserId,
|
||||
taskId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
noteTags: [],
|
||||
task: null,
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.note.findMany).mockResolvedValue(mockNotes as any);
|
||||
|
||||
const notes = await service.searchNotes(mockUserId, 'Test');
|
||||
|
||||
expect(notes).toHaveLength(1);
|
||||
expect(prisma.note.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
userId: mockUserId,
|
||||
OR: [
|
||||
{ title: { contains: 'Test' } },
|
||||
{ content: { contains: 'Test' } },
|
||||
],
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNotesStats', () => {
|
||||
it('devrait retourner les statistiques des notes', async () => {
|
||||
const mockNotes = [
|
||||
{
|
||||
id: 'note-1',
|
||||
content: 'This is a test note with words',
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 'note-2',
|
||||
content: 'Another note',
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.note.findMany).mockResolvedValue(mockNotes as any);
|
||||
|
||||
const stats = await service.getNotesStats(mockUserId);
|
||||
|
||||
expect(stats.totalNotes).toBeGreaterThanOrEqual(0);
|
||||
expect(stats.totalWords).toBeGreaterThanOrEqual(0);
|
||||
expect(stats.lastUpdated).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
276
src/services/__tests__/users.test.ts
Normal file
276
src/services/__tests__/users.test.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* Tests unitaires pour usersService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { usersService } from '../users';
|
||||
import { prisma } from '../core/database';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
// Mock de prisma
|
||||
vi.mock('../core/database', () => ({
|
||||
prisma: {
|
||||
user: {
|
||||
create: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock de bcrypt
|
||||
vi.mock('bcryptjs', () => ({
|
||||
default: {
|
||||
hash: vi.fn(),
|
||||
compare: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('usersService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createUser', () => {
|
||||
it('devrait créer un nouvel utilisateur', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
avatar: null,
|
||||
role: 'user',
|
||||
isActive: true,
|
||||
lastLoginAt: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
vi.mocked(bcrypt.hash).mockResolvedValue('hashedPassword' as any);
|
||||
vi.mocked(prisma.user.create).mockResolvedValue(mockUser as any);
|
||||
|
||||
const user = await usersService.createUser({
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
expect(user).toEqual(mockUser);
|
||||
expect(bcrypt.hash).toHaveBeenCalledWith('password123', 12);
|
||||
expect(prisma.user.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
avatar: undefined,
|
||||
role: 'user',
|
||||
password: 'hashedPassword',
|
||||
},
|
||||
select: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
it('devrait utiliser le rôle par défaut si non spécifié', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
name: null,
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
avatar: null,
|
||||
role: 'user',
|
||||
isActive: true,
|
||||
lastLoginAt: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
vi.mocked(bcrypt.hash).mockResolvedValue('hashedPassword' as any);
|
||||
vi.mocked(prisma.user.create).mockResolvedValue(mockUser as any);
|
||||
|
||||
await usersService.createUser({
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
expect(prisma.user.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
role: 'user',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserByEmail', () => {
|
||||
it('devrait récupérer un utilisateur par email', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
avatar: null,
|
||||
role: 'user',
|
||||
isActive: true,
|
||||
lastLoginAt: null,
|
||||
password: 'hashedPassword',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser as any);
|
||||
|
||||
const user = await usersService.getUserByEmail('test@example.com');
|
||||
|
||||
expect(user).toEqual(mockUser);
|
||||
expect(prisma.user.findUnique).toHaveBeenCalledWith({
|
||||
where: { email: 'test@example.com' },
|
||||
select: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
it("devrait retourner null si l'utilisateur n'existe pas", async () => {
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
|
||||
|
||||
const user = await usersService.getUserByEmail('nonexistent@example.com');
|
||||
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserById', () => {
|
||||
it('devrait récupérer un utilisateur par ID', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
avatar: null,
|
||||
role: 'user',
|
||||
isActive: true,
|
||||
lastLoginAt: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser as any);
|
||||
|
||||
const user = await usersService.getUserById('user-1');
|
||||
|
||||
expect(user).toEqual(mockUser);
|
||||
expect(prisma.user.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: 'user-1' },
|
||||
select: expect.any(Object),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyPassword', () => {
|
||||
it('devrait vérifier un mot de passe correct', async () => {
|
||||
vi.mocked(bcrypt.compare).mockResolvedValue(true as any);
|
||||
|
||||
const isValid = await usersService.verifyPassword(
|
||||
'password123',
|
||||
'hashedPassword'
|
||||
);
|
||||
|
||||
expect(isValid).toBe(true);
|
||||
expect(bcrypt.compare).toHaveBeenCalledWith(
|
||||
'password123',
|
||||
'hashedPassword'
|
||||
);
|
||||
});
|
||||
|
||||
it('devrait retourner false pour un mot de passe incorrect', async () => {
|
||||
vi.mocked(bcrypt.compare).mockResolvedValue(false as any);
|
||||
|
||||
const isValid = await usersService.verifyPassword(
|
||||
'wrongPassword',
|
||||
'hashedPassword'
|
||||
);
|
||||
|
||||
expect(isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('emailExists', () => {
|
||||
it("devrait retourner true si l'email existe", async () => {
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
} as any);
|
||||
|
||||
const exists = await usersService.emailExists('test@example.com');
|
||||
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
|
||||
it("devrait retourner false si l'email n'existe pas", async () => {
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
|
||||
|
||||
const exists = await usersService.emailExists('nonexistent@example.com');
|
||||
|
||||
expect(exists).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateLastLogin', () => {
|
||||
it('devrait mettre à jour la date de dernière connexion', async () => {
|
||||
vi.mocked(prisma.user.update).mockResolvedValue({} as any);
|
||||
|
||||
await usersService.updateLastLogin('user-1');
|
||||
|
||||
expect(prisma.user.update).toHaveBeenCalledWith({
|
||||
where: { id: 'user-1' },
|
||||
data: {
|
||||
lastLoginAt: expect.any(Date),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUser', () => {
|
||||
it('devrait mettre à jour un utilisateur', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
name: 'Updated Name',
|
||||
firstName: 'Updated',
|
||||
lastName: 'Name',
|
||||
avatar: null,
|
||||
role: 'user',
|
||||
isActive: true,
|
||||
lastLoginAt: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
vi.mocked(prisma.user.update).mockResolvedValue(mockUser as any);
|
||||
|
||||
const user = await usersService.updateUser('user-1', {
|
||||
name: 'Updated Name',
|
||||
firstName: 'Updated',
|
||||
lastName: 'Name',
|
||||
});
|
||||
|
||||
expect(user).toEqual(mockUser);
|
||||
expect(prisma.user.update).toHaveBeenCalledWith({
|
||||
where: { id: 'user-1' },
|
||||
data: {
|
||||
name: 'Updated Name',
|
||||
firstName: 'Updated',
|
||||
lastName: 'Name',
|
||||
},
|
||||
select: expect.any(Object),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
278
src/services/analytics/__tests__/analytics.test.ts
Normal file
278
src/services/analytics/__tests__/analytics.test.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* Tests unitaires pour AnalyticsService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { AnalyticsService } from '../analytics';
|
||||
import { TaskStatus, TaskPriority, TaskSource } from '@/lib/types';
|
||||
import { prisma } from '@/services/core/database';
|
||||
|
||||
// Mock de prisma
|
||||
vi.mock('@/services/core/database', () => ({
|
||||
prisma: {
|
||||
task: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('AnalyticsService', () => {
|
||||
const userId = 'test-user-id';
|
||||
const mockDate = new Date('2024-01-15T12:00:00Z');
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(mockDate);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('getProductivityMetrics', () => {
|
||||
it('devrait calculer les métriques de productivité avec des tâches', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche 1',
|
||||
description: 'Description 1',
|
||||
status: 'done' as TaskStatus,
|
||||
priority: 'high' as TaskPriority,
|
||||
source: 'manual' as TaskSource,
|
||||
sourceId: null,
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-12'),
|
||||
completedAt: new Date('2024-01-12'),
|
||||
dueDate: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tâche 2',
|
||||
description: 'Description 2',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium' as TaskPriority,
|
||||
source: 'jira' as TaskSource,
|
||||
sourceId: 'JIRA-123',
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-13'),
|
||||
updatedAt: new Date('2024-01-13'),
|
||||
completedAt: null,
|
||||
dueDate: null,
|
||||
jiraProject: 'PROJ',
|
||||
jiraKey: 'JIRA-123',
|
||||
jiraType: 'Task',
|
||||
assignee: null,
|
||||
taskTags: [],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
|
||||
const result = await AnalyticsService.getProductivityMetrics(userId);
|
||||
|
||||
expect(result).toHaveProperty('completionTrend');
|
||||
expect(result).toHaveProperty('velocityData');
|
||||
expect(result).toHaveProperty('priorityDistribution');
|
||||
expect(result).toHaveProperty('statusFlow');
|
||||
expect(result).toHaveProperty('weeklyStats');
|
||||
|
||||
expect(result.completionTrend.length).toBeGreaterThan(0);
|
||||
expect(result.priorityDistribution.length).toBeGreaterThan(0);
|
||||
expect(result.statusFlow.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('devrait gérer une liste vide de tâches', async () => {
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue([]);
|
||||
|
||||
const result = await AnalyticsService.getProductivityMetrics(userId);
|
||||
|
||||
expect(result.completionTrend.length).toBeGreaterThan(0);
|
||||
expect(result.velocityData).toEqual([]);
|
||||
expect(result.priorityDistribution).toEqual([]);
|
||||
expect(result.statusFlow).toEqual([]);
|
||||
});
|
||||
|
||||
it('devrait filtrer par sources si spécifié', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche Jira',
|
||||
status: 'done' as TaskStatus,
|
||||
priority: 'high' as TaskPriority,
|
||||
source: 'jira' as TaskSource,
|
||||
sourceId: 'JIRA-1',
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-10'),
|
||||
completedAt: new Date('2024-01-10'),
|
||||
dueDate: null,
|
||||
description: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tâche Manual',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium' as TaskPriority,
|
||||
source: 'manual' as TaskSource,
|
||||
sourceId: null,
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-10'),
|
||||
completedAt: null,
|
||||
dueDate: null,
|
||||
description: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
|
||||
const result = await AnalyticsService.getProductivityMetrics(
|
||||
userId,
|
||||
undefined,
|
||||
['jira']
|
||||
);
|
||||
|
||||
// Vérifier que seules les tâches Jira sont incluses dans les calculs
|
||||
const allStatuses = result.statusFlow.map((s) => s.status);
|
||||
expect(allStatuses).toContain('Terminé'); // Seule la tâche Jira est done
|
||||
});
|
||||
|
||||
it('devrait utiliser une période personnalisée si fournie', async () => {
|
||||
const startDate = new Date('2024-01-01');
|
||||
const endDate = new Date('2024-01-10');
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue([]);
|
||||
|
||||
await AnalyticsService.getProductivityMetrics(userId, {
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
});
|
||||
|
||||
expect(prisma.task.findMany).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait calculer correctement la tendance de completion', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche complétée',
|
||||
status: 'done' as TaskStatus,
|
||||
priority: 'high' as TaskPriority,
|
||||
source: 'manual' as TaskSource,
|
||||
sourceId: null,
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-12'),
|
||||
completedAt: new Date('2024-01-12'),
|
||||
dueDate: null,
|
||||
description: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
|
||||
const result = await AnalyticsService.getProductivityMetrics(userId);
|
||||
|
||||
const completionDay = result.completionTrend.find(
|
||||
(day) => day.date === '2024-01-12'
|
||||
);
|
||||
expect(completionDay).toBeDefined();
|
||||
expect(completionDay?.completed).toBe(1);
|
||||
});
|
||||
|
||||
it('devrait calculer correctement la distribution des priorités', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche haute priorité',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'high' as TaskPriority,
|
||||
source: 'manual' as TaskSource,
|
||||
sourceId: null,
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-10'),
|
||||
completedAt: null,
|
||||
dueDate: null,
|
||||
description: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tâche moyenne priorité',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium' as TaskPriority,
|
||||
source: 'manual' as TaskSource,
|
||||
sourceId: null,
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-10'),
|
||||
completedAt: null,
|
||||
dueDate: null,
|
||||
description: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
|
||||
const result = await AnalyticsService.getProductivityMetrics(userId);
|
||||
|
||||
const highPriority = result.priorityDistribution.find(
|
||||
(p) => p.priority === 'Élevée'
|
||||
);
|
||||
const mediumPriority = result.priorityDistribution.find(
|
||||
(p) => p.priority === 'Moyenne'
|
||||
);
|
||||
|
||||
expect(highPriority).toBeDefined();
|
||||
expect(highPriority?.count).toBe(1);
|
||||
expect(highPriority?.percentage).toBe(50);
|
||||
|
||||
expect(mediumPriority).toBeDefined();
|
||||
expect(mediumPriority?.count).toBe(1);
|
||||
expect(mediumPriority?.percentage).toBe(50);
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs de base de données', async () => {
|
||||
vi.mocked(prisma.task.findMany).mockRejectedValue(
|
||||
new Error('Database error')
|
||||
);
|
||||
|
||||
await expect(
|
||||
AnalyticsService.getProductivityMetrics(userId)
|
||||
).rejects.toThrow('Impossible de calculer les métriques de productivité');
|
||||
});
|
||||
});
|
||||
});
|
||||
380
src/services/analytics/__tests__/deadline-analytics.test.ts
Normal file
380
src/services/analytics/__tests__/deadline-analytics.test.ts
Normal file
@@ -0,0 +1,380 @@
|
||||
/**
|
||||
* Tests unitaires pour DeadlineAnalyticsService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { DeadlineAnalyticsService } from '../deadline-analytics';
|
||||
import { TaskStatus } from '@/lib/types';
|
||||
import { prisma } from '@/services/core/database';
|
||||
|
||||
// Mock de prisma
|
||||
vi.mock('@/services/core/database', () => ({
|
||||
prisma: {
|
||||
task: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('DeadlineAnalyticsService', () => {
|
||||
const userId = 'test-user-id';
|
||||
const mockDate = new Date('2024-01-15T12:00:00Z');
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(mockDate);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('getDeadlineMetrics', () => {
|
||||
it('devrait catégoriser les tâches par urgence', async () => {
|
||||
const overdueDate = new Date('2024-01-10'); // 5 jours en retard
|
||||
const criticalDate = new Date('2024-01-16'); // 1 jour restant
|
||||
const warningDate = new Date('2024-01-20'); // 5 jours restants
|
||||
const upcomingDate = new Date('2024-01-25'); // 10 jours restants
|
||||
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche en retard',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'high',
|
||||
dueDate: overdueDate,
|
||||
source: 'manual',
|
||||
ownerId: userId,
|
||||
taskTags: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tâche critique',
|
||||
status: 'in_progress' as TaskStatus,
|
||||
priority: 'urgent',
|
||||
dueDate: criticalDate,
|
||||
source: 'jira',
|
||||
ownerId: userId,
|
||||
taskTags: [],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Tâche warning',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium',
|
||||
dueDate: warningDate,
|
||||
source: 'manual',
|
||||
ownerId: userId,
|
||||
taskTags: [],
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Tâche à venir',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'low',
|
||||
dueDate: upcomingDate,
|
||||
source: 'manual',
|
||||
ownerId: userId,
|
||||
taskTags: [],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
|
||||
const result = await DeadlineAnalyticsService.getDeadlineMetrics(userId);
|
||||
|
||||
expect(result.overdue).toHaveLength(1);
|
||||
expect(result.overdue[0].id).toBe('1');
|
||||
expect(result.critical).toHaveLength(1);
|
||||
expect(result.critical[0].id).toBe('2');
|
||||
expect(result.warning).toHaveLength(1);
|
||||
expect(result.warning[0].id).toBe('3');
|
||||
expect(result.upcoming).toHaveLength(1);
|
||||
expect(result.upcoming[0].id).toBe('4');
|
||||
|
||||
expect(result.summary.overdueCount).toBe(1);
|
||||
expect(result.summary.criticalCount).toBe(1);
|
||||
expect(result.summary.warningCount).toBe(1);
|
||||
expect(result.summary.upcomingCount).toBe(1);
|
||||
expect(result.summary.totalWithDeadlines).toBe(4);
|
||||
});
|
||||
|
||||
it('devrait exclure les tâches terminées ou annulées', async () => {
|
||||
// Le service filtre déjà les tâches terminées dans la requête Prisma
|
||||
// donc le mock devrait retourner une liste vide
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue([]);
|
||||
|
||||
const result = await DeadlineAnalyticsService.getDeadlineMetrics(userId);
|
||||
|
||||
expect(result.summary.totalWithDeadlines).toBe(0);
|
||||
});
|
||||
|
||||
it('devrait filtrer par sources si spécifié', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche Jira',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'high',
|
||||
dueDate: new Date('2024-01-16'),
|
||||
source: 'jira',
|
||||
ownerId: userId,
|
||||
taskTags: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tâche Manual',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium',
|
||||
dueDate: new Date('2024-01-17'),
|
||||
source: 'manual',
|
||||
ownerId: userId,
|
||||
taskTags: [],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
|
||||
const result = await DeadlineAnalyticsService.getDeadlineMetrics(userId, [
|
||||
'jira',
|
||||
]);
|
||||
|
||||
const allTaskIds = [
|
||||
...result.overdue,
|
||||
...result.critical,
|
||||
...result.warning,
|
||||
...result.upcoming,
|
||||
].map((t) => t.id);
|
||||
|
||||
expect(allTaskIds).toContain('1');
|
||||
expect(allTaskIds).not.toContain('2');
|
||||
});
|
||||
|
||||
it('devrait calculer correctement les jours restants', async () => {
|
||||
// Utiliser une date normalisée pour éviter les problèmes de timezone
|
||||
// La date mockée est le 15 janvier à 12:00, donc le 20 janvier à 00:00
|
||||
// donne environ 4-5 jours selon le calcul avec Math.ceil
|
||||
const dueDate = new Date('2024-01-20T00:00:00Z'); // 5 jours dans le futur
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche test',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium',
|
||||
dueDate,
|
||||
source: 'manual',
|
||||
ownerId: userId,
|
||||
taskTags: [],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
|
||||
const result = await DeadlineAnalyticsService.getDeadlineMetrics(userId);
|
||||
|
||||
// Le calcul utilise Math.ceil, donc avec les heures ça peut donner 5 ou 6 jours
|
||||
// Vérifions que c'est dans la plage warning (3-7 jours)
|
||||
expect(result.warning[0].daysRemaining).toBeGreaterThanOrEqual(5);
|
||||
expect(result.warning[0].daysRemaining).toBeLessThanOrEqual(6);
|
||||
expect(result.warning[0].urgencyLevel).toBe('warning');
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs de base de données', async () => {
|
||||
vi.mocked(prisma.task.findMany).mockRejectedValue(
|
||||
new Error('Database error')
|
||||
);
|
||||
|
||||
await expect(
|
||||
DeadlineAnalyticsService.getDeadlineMetrics(userId)
|
||||
).rejects.toThrow("Impossible d'analyser les échéances");
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCriticalDeadlines', () => {
|
||||
it('devrait retourner les 10 tâches les plus critiques', async () => {
|
||||
const mockTasks = Array.from({ length: 15 }, (_, i) => ({
|
||||
id: `${i + 1}`,
|
||||
title: `Tâche ${i + 1}`,
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'high',
|
||||
dueDate: new Date(`2024-01-${10 + i}`), // Dates en retard
|
||||
source: 'manual',
|
||||
ownerId: userId,
|
||||
taskTags: [],
|
||||
}));
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
|
||||
const result =
|
||||
await DeadlineAnalyticsService.getCriticalDeadlines(userId);
|
||||
|
||||
expect(result.length).toBeLessThanOrEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('analyzeImpactByPriority', () => {
|
||||
it("devrait analyser l'impact par priorité", () => {
|
||||
const tasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche haute priorité en retard',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'high',
|
||||
dueDate: new Date('2024-01-10'),
|
||||
daysRemaining: -5,
|
||||
urgencyLevel: 'overdue' as const,
|
||||
source: 'manual',
|
||||
tags: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tâche haute priorité critique',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'high',
|
||||
dueDate: new Date('2024-01-16'),
|
||||
daysRemaining: 1,
|
||||
urgencyLevel: 'critical' as const,
|
||||
source: 'manual',
|
||||
tags: [],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Tâche moyenne priorité',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium',
|
||||
dueDate: new Date('2024-01-20'),
|
||||
daysRemaining: 5,
|
||||
urgencyLevel: 'warning' as const,
|
||||
source: 'manual',
|
||||
tags: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = DeadlineAnalyticsService.analyzeImpactByPriority(tasks);
|
||||
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
const highPriority = result.find((p) => p.priority === 'high');
|
||||
expect(highPriority).toBeDefined();
|
||||
expect(highPriority?.overdueCount).toBe(1);
|
||||
expect(highPriority?.criticalCount).toBe(1);
|
||||
});
|
||||
|
||||
it('devrait trier par impact décroissant', () => {
|
||||
const tasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche haute priorité',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'high',
|
||||
dueDate: new Date('2024-01-10'),
|
||||
daysRemaining: -5,
|
||||
urgencyLevel: 'overdue' as const,
|
||||
source: 'manual',
|
||||
tags: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tâche basse priorité',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'low',
|
||||
dueDate: new Date('2024-01-20'),
|
||||
daysRemaining: 5,
|
||||
urgencyLevel: 'warning' as const,
|
||||
source: 'manual',
|
||||
tags: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = DeadlineAnalyticsService.analyzeImpactByPriority(tasks);
|
||||
|
||||
expect(result[0].priority).toBe('high'); // Plus d'impact
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateRiskMetrics', () => {
|
||||
it('devrait calculer un score de risque faible', () => {
|
||||
const metrics = {
|
||||
overdue: [],
|
||||
critical: [],
|
||||
warning: [],
|
||||
upcoming: [],
|
||||
summary: {
|
||||
overdueCount: 0,
|
||||
criticalCount: 0,
|
||||
warningCount: 1,
|
||||
upcomingCount: 2,
|
||||
totalWithDeadlines: 3,
|
||||
},
|
||||
};
|
||||
|
||||
const result = DeadlineAnalyticsService.calculateRiskMetrics(metrics);
|
||||
|
||||
expect(result.riskScore).toBeLessThan(25);
|
||||
expect(result.riskLevel).toBe('low');
|
||||
});
|
||||
|
||||
it('devrait calculer un score de risque critique', () => {
|
||||
const metrics = {
|
||||
overdue: [],
|
||||
critical: [],
|
||||
warning: [],
|
||||
upcoming: [],
|
||||
summary: {
|
||||
overdueCount: 3,
|
||||
criticalCount: 2,
|
||||
warningCount: 0,
|
||||
upcomingCount: 0,
|
||||
totalWithDeadlines: 5,
|
||||
},
|
||||
};
|
||||
|
||||
const result = DeadlineAnalyticsService.calculateRiskMetrics(metrics);
|
||||
|
||||
expect(result.riskScore).toBeGreaterThanOrEqual(75);
|
||||
expect(result.riskLevel).toBe('critical');
|
||||
});
|
||||
|
||||
it('devrait limiter le score à 100', () => {
|
||||
const metrics = {
|
||||
overdue: [],
|
||||
critical: [],
|
||||
warning: [],
|
||||
upcoming: [],
|
||||
summary: {
|
||||
overdueCount: 10,
|
||||
criticalCount: 10,
|
||||
warningCount: 10,
|
||||
upcomingCount: 10,
|
||||
totalWithDeadlines: 40,
|
||||
},
|
||||
};
|
||||
|
||||
const result = DeadlineAnalyticsService.calculateRiskMetrics(metrics);
|
||||
|
||||
expect(result.riskScore).toBeLessThanOrEqual(100);
|
||||
});
|
||||
|
||||
it('devrait fournir des recommandations appropriées', () => {
|
||||
const metrics = {
|
||||
overdue: [],
|
||||
critical: [],
|
||||
warning: [],
|
||||
upcoming: [],
|
||||
summary: {
|
||||
overdueCount: 2,
|
||||
criticalCount: 1,
|
||||
warningCount: 2,
|
||||
upcomingCount: 1,
|
||||
totalWithDeadlines: 6,
|
||||
},
|
||||
};
|
||||
|
||||
const result = DeadlineAnalyticsService.calculateRiskMetrics(metrics);
|
||||
|
||||
expect(result.recommendation).toBeDefined();
|
||||
expect(result.recommendation.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
314
src/services/analytics/__tests__/manager-summary.test.ts
Normal file
314
src/services/analytics/__tests__/manager-summary.test.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* Tests unitaires pour ManagerSummaryService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { ManagerSummaryService } from '../manager-summary';
|
||||
import { prisma } from '@/services/core/database';
|
||||
|
||||
// Mock de prisma
|
||||
vi.mock('@/services/core/database', () => ({
|
||||
prisma: {
|
||||
task: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
dailyCheckbox: {
|
||||
findMany: vi.fn(),
|
||||
count: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ManagerSummaryService', () => {
|
||||
const userId = 'test-user-id';
|
||||
const mockDate = new Date('2024-01-15T12:00:00Z');
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(mockDate);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('getManagerSummary', () => {
|
||||
it('devrait générer un résumé pour les 7 derniers jours', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche complétée',
|
||||
description: 'Description',
|
||||
priority: 'high',
|
||||
completedAt: new Date('2024-01-12'),
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-12'),
|
||||
taskTags: [],
|
||||
},
|
||||
];
|
||||
|
||||
const mockCheckboxes = [
|
||||
{
|
||||
id: '1',
|
||||
text: 'Todo complété',
|
||||
isChecked: true,
|
||||
type: 'task',
|
||||
date: new Date('2024-01-13'),
|
||||
createdAt: new Date('2024-01-13'),
|
||||
task: null,
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue(
|
||||
mockCheckboxes as any
|
||||
);
|
||||
vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0);
|
||||
|
||||
const result = await ManagerSummaryService.getManagerSummary(userId);
|
||||
|
||||
expect(result).toHaveProperty('period');
|
||||
expect(result).toHaveProperty('keyAccomplishments');
|
||||
expect(result).toHaveProperty('upcomingChallenges');
|
||||
expect(result).toHaveProperty('metrics');
|
||||
expect(result).toHaveProperty('narrative');
|
||||
|
||||
expect(result.period.start).toBeInstanceOf(Date);
|
||||
expect(result.period.end).toBeInstanceOf(Date);
|
||||
expect(result.keyAccomplishments.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('devrait extraire les accomplissements clés des tâches', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche haute priorité',
|
||||
description: 'Description importante',
|
||||
priority: 'high',
|
||||
completedAt: new Date('2024-01-12'),
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-12'),
|
||||
taskTags: [
|
||||
{
|
||||
tag: {
|
||||
name: 'important',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue([]);
|
||||
vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0);
|
||||
|
||||
const result = await ManagerSummaryService.getManagerSummary(userId);
|
||||
|
||||
const accomplishment = result.keyAccomplishments.find(
|
||||
(a) => a.id === 'task-1'
|
||||
);
|
||||
expect(accomplishment).toBeDefined();
|
||||
expect(accomplishment?.impact).toBe('high');
|
||||
expect(accomplishment?.tags).toContain('important');
|
||||
});
|
||||
|
||||
it('devrait inclure les todos standalone comme accomplissements', async () => {
|
||||
const mockCheckboxes = [
|
||||
{
|
||||
id: '1',
|
||||
text: 'Réunion importante',
|
||||
isChecked: true,
|
||||
type: 'meeting',
|
||||
date: new Date('2024-01-13'),
|
||||
createdAt: new Date('2024-01-13'),
|
||||
task: null,
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue([]);
|
||||
vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue(
|
||||
mockCheckboxes as any
|
||||
);
|
||||
|
||||
const result = await ManagerSummaryService.getManagerSummary(userId);
|
||||
|
||||
const accomplishment = result.keyAccomplishments.find(
|
||||
(a) => a.id === 'todo-1'
|
||||
);
|
||||
expect(accomplishment).toBeDefined();
|
||||
expect(accomplishment?.title).toContain('Réunion importante');
|
||||
expect(accomplishment?.impact).toBe('low');
|
||||
});
|
||||
|
||||
it('devrait identifier les défis à venir', async () => {
|
||||
const mockUpcomingTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche à venir',
|
||||
description: 'Description',
|
||||
priority: 'high',
|
||||
createdAt: new Date('2024-01-10'),
|
||||
taskTags: [],
|
||||
},
|
||||
];
|
||||
|
||||
const mockUpcomingCheckboxes = [
|
||||
{
|
||||
id: '1',
|
||||
text: 'Todo à venir',
|
||||
isChecked: false,
|
||||
type: 'task',
|
||||
date: new Date('2024-01-20'),
|
||||
createdAt: new Date('2024-01-10'),
|
||||
task: {
|
||||
id: '1',
|
||||
title: 'Tâche à venir',
|
||||
priority: 'high',
|
||||
taskTags: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Mock pour getCompletedTasks
|
||||
vi.mocked(prisma.task.findMany)
|
||||
.mockResolvedValueOnce([]) // Tâches complétées
|
||||
.mockResolvedValueOnce(mockUpcomingTasks as any); // Tâches à venir
|
||||
|
||||
vi.mocked(prisma.dailyCheckbox.findMany)
|
||||
.mockResolvedValueOnce([]) // Checkboxes complétées
|
||||
.mockResolvedValueOnce(mockUpcomingCheckboxes as any); // Checkboxes à venir
|
||||
|
||||
vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0);
|
||||
|
||||
const result = await ManagerSummaryService.getManagerSummary(userId);
|
||||
|
||||
expect(result.upcomingChallenges.length).toBeGreaterThan(0);
|
||||
const challenge = result.upcomingChallenges.find(
|
||||
(c) => c.id === 'task-1'
|
||||
);
|
||||
expect(challenge).toBeDefined();
|
||||
expect(challenge?.priority).toBe('high');
|
||||
});
|
||||
|
||||
it('devrait calculer les métriques correctement', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche haute priorité',
|
||||
description: 'Description',
|
||||
priority: 'high',
|
||||
completedAt: new Date('2024-01-12'),
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-12'),
|
||||
taskTags: [],
|
||||
},
|
||||
];
|
||||
|
||||
const mockCheckboxes = [
|
||||
{
|
||||
id: '1',
|
||||
text: 'Réunion',
|
||||
isChecked: true,
|
||||
type: 'meeting',
|
||||
date: new Date('2024-01-13'),
|
||||
createdAt: new Date('2024-01-13'),
|
||||
task: null,
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue(
|
||||
mockCheckboxes as any
|
||||
);
|
||||
vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0);
|
||||
|
||||
const result = await ManagerSummaryService.getManagerSummary(userId);
|
||||
|
||||
expect(result.metrics.totalTasksCompleted).toBe(1);
|
||||
expect(result.metrics.totalCheckboxesCompleted).toBe(1);
|
||||
expect(result.metrics.highPriorityTasksCompleted).toBe(1);
|
||||
expect(result.metrics.meetingCheckboxesCompleted).toBe(1);
|
||||
});
|
||||
|
||||
it('devrait générer un narratif', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche importante',
|
||||
description: 'Description',
|
||||
priority: 'high',
|
||||
completedAt: new Date('2024-01-12'),
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-12'),
|
||||
taskTags: [
|
||||
{
|
||||
tag: {
|
||||
name: 'important',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue([]);
|
||||
vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0);
|
||||
|
||||
const result = await ManagerSummaryService.getManagerSummary(userId);
|
||||
|
||||
expect(result.narrative).toHaveProperty('weekHighlight');
|
||||
expect(result.narrative).toHaveProperty('mainChallenges');
|
||||
expect(result.narrative).toHaveProperty('nextWeekFocus');
|
||||
expect(result.narrative.weekHighlight.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('devrait utiliser une date personnalisée si fournie', async () => {
|
||||
const customDate = new Date('2024-02-01');
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue([]);
|
||||
vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue([]);
|
||||
|
||||
const result = await ManagerSummaryService.getManagerSummary(
|
||||
userId,
|
||||
customDate
|
||||
);
|
||||
|
||||
expect(result.period.end).toEqual(customDate);
|
||||
});
|
||||
|
||||
it('devrait trier les accomplissements par impact', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche haute priorité',
|
||||
description: 'Description',
|
||||
priority: 'high',
|
||||
completedAt: new Date('2024-01-12'),
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-12'),
|
||||
taskTags: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tâche basse priorité',
|
||||
description: 'Description',
|
||||
priority: 'low',
|
||||
completedAt: new Date('2024-01-13'),
|
||||
createdAt: new Date('2024-01-11'),
|
||||
updatedAt: new Date('2024-01-13'),
|
||||
taskTags: [],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue([]);
|
||||
vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0);
|
||||
|
||||
const result = await ManagerSummaryService.getManagerSummary(userId);
|
||||
|
||||
expect(result.keyAccomplishments[0].impact).toBe('high');
|
||||
});
|
||||
});
|
||||
});
|
||||
234
src/services/analytics/__tests__/metrics.test.ts
Normal file
234
src/services/analytics/__tests__/metrics.test.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* Tests unitaires pour MetricsService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { MetricsService } from '../metrics';
|
||||
import { prisma } from '@/services/core/database';
|
||||
|
||||
// Mock de prisma
|
||||
vi.mock('@/services/core/database', () => ({
|
||||
prisma: {
|
||||
task: {
|
||||
count: vi.fn(),
|
||||
groupBy: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('MetricsService', () => {
|
||||
const userId = 'test-user-id';
|
||||
const mockDate = new Date('2024-01-15T12:00:00Z');
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(mockDate);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('getWeeklyMetrics', () => {
|
||||
it('devrait retourner les métriques hebdomadaires', async () => {
|
||||
// Mock des comptages pour chaque jour
|
||||
vi.mocked(prisma.task.count)
|
||||
.mockResolvedValueOnce(5) // completed
|
||||
.mockResolvedValueOnce(3) // inProgress
|
||||
.mockResolvedValueOnce(1) // blocked
|
||||
.mockResolvedValueOnce(2) // pending
|
||||
.mockResolvedValueOnce(2) // newTasks
|
||||
.mockResolvedValueOnce(10) // totalTasks
|
||||
.mockResolvedValueOnce(5) // completed jour 2
|
||||
.mockResolvedValueOnce(3) // inProgress jour 2
|
||||
.mockResolvedValueOnce(1) // blocked jour 2
|
||||
.mockResolvedValueOnce(2) // pending jour 2
|
||||
.mockResolvedValueOnce(2) // newTasks jour 2
|
||||
.mockResolvedValueOnce(10); // totalTasks jour 2
|
||||
|
||||
// Mock pour les autres jours (répéter pour 7 jours)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
vi.mocked(prisma.task.count)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0);
|
||||
}
|
||||
|
||||
// Mock pour statusDistribution
|
||||
vi.mocked(prisma.task.groupBy).mockResolvedValueOnce([
|
||||
{
|
||||
status: 'done',
|
||||
_count: { status: 5 },
|
||||
},
|
||||
{
|
||||
status: 'in_progress',
|
||||
_count: { status: 3 },
|
||||
},
|
||||
] as any);
|
||||
|
||||
// Mock pour priorityBreakdown (3 priorités × 1 count each)
|
||||
vi.mocked(prisma.task.count)
|
||||
.mockResolvedValueOnce(2) // high completed
|
||||
.mockResolvedValueOnce(1) // high inProgress
|
||||
.mockResolvedValueOnce(2) // medium completed
|
||||
.mockResolvedValueOnce(1) // medium inProgress
|
||||
.mockResolvedValueOnce(1) // low completed
|
||||
.mockResolvedValueOnce(1); // low inProgress
|
||||
|
||||
const result = await MetricsService.getWeeklyMetrics(userId);
|
||||
|
||||
expect(result).toHaveProperty('period');
|
||||
expect(result).toHaveProperty('dailyBreakdown');
|
||||
expect(result).toHaveProperty('summary');
|
||||
expect(result).toHaveProperty('statusDistribution');
|
||||
expect(result).toHaveProperty('priorityBreakdown');
|
||||
|
||||
expect(result.dailyBreakdown).toHaveLength(7);
|
||||
expect(result.summary).toHaveProperty('totalTasksCompleted');
|
||||
expect(result.summary).toHaveProperty('totalTasksCreated');
|
||||
expect(result.summary).toHaveProperty('averageCompletionRate');
|
||||
});
|
||||
|
||||
it('devrait calculer correctement le taux de completion', async () => {
|
||||
// Mock pour un jour avec des tâches complétées
|
||||
vi.mocked(prisma.task.count)
|
||||
.mockResolvedValueOnce(5) // completed
|
||||
.mockResolvedValueOnce(0) // inProgress
|
||||
.mockResolvedValueOnce(0) // blocked
|
||||
.mockResolvedValueOnce(0) // pending
|
||||
.mockResolvedValueOnce(0) // newTasks
|
||||
.mockResolvedValueOnce(10); // totalTasks
|
||||
|
||||
// Mock pour les autres jours
|
||||
for (let i = 0; i < 6; i++) {
|
||||
vi.mocked(prisma.task.count)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0);
|
||||
}
|
||||
|
||||
vi.mocked(prisma.task.groupBy).mockResolvedValueOnce([] as any);
|
||||
|
||||
// Mock priorityBreakdown
|
||||
vi.mocked(prisma.task.count)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0);
|
||||
|
||||
const result = await MetricsService.getWeeklyMetrics(userId);
|
||||
|
||||
expect(result.dailyBreakdown[0].completionRate).toBe(50);
|
||||
});
|
||||
|
||||
it('devrait identifier le jour de pic de productivité', async () => {
|
||||
// Jour 1: 10 complétées
|
||||
vi.mocked(prisma.task.count)
|
||||
.mockResolvedValueOnce(10)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(10);
|
||||
|
||||
// Jour 2-7: 0 complétées
|
||||
for (let i = 0; i < 6; i++) {
|
||||
vi.mocked(prisma.task.count)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0);
|
||||
}
|
||||
|
||||
vi.mocked(prisma.task.groupBy).mockResolvedValueOnce([] as any);
|
||||
|
||||
// Mock priorityBreakdown
|
||||
vi.mocked(prisma.task.count)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0)
|
||||
.mockResolvedValueOnce(0);
|
||||
|
||||
const result = await MetricsService.getWeeklyMetrics(userId);
|
||||
|
||||
expect(result.summary.totalTasksCompleted).toBe(10);
|
||||
expect(result.summary.peakProductivityDay).toBeDefined();
|
||||
});
|
||||
|
||||
it('devrait utiliser une date personnalisée si fournie', async () => {
|
||||
const customDate = new Date('2024-02-01');
|
||||
|
||||
// Mock minimal pour éviter les erreurs
|
||||
vi.mocked(prisma.task.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.task.groupBy).mockResolvedValue([]);
|
||||
|
||||
await MetricsService.getWeeklyMetrics(userId, customDate);
|
||||
|
||||
expect(prisma.task.count).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVelocityTrends', () => {
|
||||
it('devrait retourner les tendances de vélocité', async () => {
|
||||
vi.mocked(prisma.task.count)
|
||||
.mockResolvedValueOnce(5) // completed semaine 1
|
||||
.mockResolvedValueOnce(3) // created semaine 1
|
||||
.mockResolvedValueOnce(7) // completed semaine 2
|
||||
.mockResolvedValueOnce(4) // created semaine 2
|
||||
.mockResolvedValueOnce(6) // completed semaine 3
|
||||
.mockResolvedValueOnce(5) // created semaine 3
|
||||
.mockResolvedValueOnce(8) // completed semaine 4
|
||||
.mockResolvedValueOnce(6); // created semaine 4
|
||||
|
||||
const result = await MetricsService.getVelocityTrends(userId, 4);
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result[0]).toHaveProperty('date');
|
||||
expect(result[0]).toHaveProperty('completed');
|
||||
expect(result[0]).toHaveProperty('created');
|
||||
expect(result[0]).toHaveProperty('velocity');
|
||||
});
|
||||
|
||||
it('devrait calculer correctement la vélocité', async () => {
|
||||
vi.mocked(prisma.task.count)
|
||||
.mockResolvedValueOnce(10) // completed
|
||||
.mockResolvedValueOnce(5); // created
|
||||
|
||||
const result = await MetricsService.getVelocityTrends(userId, 1);
|
||||
|
||||
expect(result[0].velocity).toBe(200); // 10/5 * 100
|
||||
});
|
||||
|
||||
it("devrait gérer le cas où aucune tâche n'est créée", async () => {
|
||||
vi.mocked(prisma.task.count)
|
||||
.mockResolvedValueOnce(5) // completed
|
||||
.mockResolvedValueOnce(0); // created
|
||||
|
||||
const result = await MetricsService.getVelocityTrends(userId, 1);
|
||||
|
||||
expect(result[0].velocity).toBe(0);
|
||||
});
|
||||
|
||||
it('devrait utiliser le nombre de semaines personnalisé', async () => {
|
||||
vi.mocked(prisma.task.count).mockResolvedValue(0);
|
||||
|
||||
const result = await MetricsService.getVelocityTrends(userId, 8);
|
||||
|
||||
expect(result).toHaveLength(8);
|
||||
});
|
||||
});
|
||||
});
|
||||
502
src/services/analytics/__tests__/tag-analytics.test.ts
Normal file
502
src/services/analytics/__tests__/tag-analytics.test.ts
Normal file
@@ -0,0 +1,502 @@
|
||||
/**
|
||||
* Tests unitaires pour TagAnalyticsService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { TagAnalyticsService } from '../tag-analytics';
|
||||
import { TaskStatus, TaskPriority, TaskSource } from '@/lib/types';
|
||||
import { prisma } from '@/services/core/database';
|
||||
|
||||
// Mock de prisma
|
||||
vi.mock('@/services/core/database', () => ({
|
||||
prisma: {
|
||||
task: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
tag: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('TagAnalyticsService', () => {
|
||||
const userId = 'test-user-id';
|
||||
const mockDate = new Date('2024-01-15T12:00:00Z');
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(mockDate);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('getTagDistributionMetrics', () => {
|
||||
it('devrait calculer la distribution des tags', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche avec tag',
|
||||
description: 'Description',
|
||||
status: 'done' as TaskStatus,
|
||||
priority: 'high' as TaskPriority,
|
||||
source: 'manual' as TaskSource,
|
||||
sourceId: null,
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-12'),
|
||||
completedAt: new Date('2024-01-12'),
|
||||
dueDate: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [
|
||||
{
|
||||
tag: {
|
||||
name: 'important',
|
||||
color: '#ff0000',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Autre tâche',
|
||||
description: 'Description',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium' as TaskPriority,
|
||||
source: 'manual' as TaskSource,
|
||||
sourceId: null,
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-11'),
|
||||
updatedAt: new Date('2024-01-11'),
|
||||
completedAt: null,
|
||||
dueDate: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [
|
||||
{
|
||||
tag: {
|
||||
name: 'important',
|
||||
color: '#ff0000',
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: {
|
||||
name: 'urgent',
|
||||
color: '#00ff00',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mockTags = [
|
||||
{ name: 'important', color: '#ff0000' },
|
||||
{ name: 'urgent', color: '#00ff00' },
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
vi.mocked(prisma.tag.findMany).mockResolvedValue(mockTags as any);
|
||||
|
||||
const result =
|
||||
await TagAnalyticsService.getTagDistributionMetrics(userId);
|
||||
|
||||
expect(result).toHaveProperty('tagDistribution');
|
||||
expect(result).toHaveProperty('topTags');
|
||||
expect(result).toHaveProperty('tagTrends');
|
||||
expect(result).toHaveProperty('tagStats');
|
||||
|
||||
expect(result.tagDistribution.length).toBeGreaterThan(0);
|
||||
const importantTag = result.tagDistribution.find(
|
||||
(t) => t.tagName === 'important'
|
||||
);
|
||||
expect(importantTag).toBeDefined();
|
||||
expect(importantTag?.count).toBe(2);
|
||||
expect(importantTag?.percentage).toBe(100); // 2 tâches sur 2
|
||||
});
|
||||
|
||||
it('devrait calculer les top tags avec métriques', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche complétée',
|
||||
status: 'done' as TaskStatus,
|
||||
priority: 'high' as TaskPriority,
|
||||
source: 'manual' as TaskSource,
|
||||
sourceId: null,
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-12'),
|
||||
completedAt: new Date('2024-01-12'),
|
||||
dueDate: null,
|
||||
description: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [
|
||||
{
|
||||
tag: {
|
||||
name: 'important',
|
||||
color: '#ff0000',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tâche non complétée',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium' as TaskPriority,
|
||||
source: 'manual' as TaskSource,
|
||||
sourceId: null,
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-11'),
|
||||
updatedAt: new Date('2024-01-11'),
|
||||
completedAt: null,
|
||||
dueDate: null,
|
||||
description: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [
|
||||
{
|
||||
tag: {
|
||||
name: 'important',
|
||||
color: '#ff0000',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mockTags = [{ name: 'important', color: '#ff0000' }];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
vi.mocked(prisma.tag.findMany).mockResolvedValue(mockTags as any);
|
||||
|
||||
const result =
|
||||
await TagAnalyticsService.getTagDistributionMetrics(userId);
|
||||
|
||||
const importantTag = result.topTags.find(
|
||||
(t) => t.tagName === 'important'
|
||||
);
|
||||
expect(importantTag).toBeDefined();
|
||||
expect(importantTag?.usage).toBe(2);
|
||||
expect(importantTag?.completionRate).toBe(50); // 1 complétée sur 2
|
||||
});
|
||||
|
||||
it('devrait calculer les tendances des tags', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium' as TaskPriority,
|
||||
source: 'manual' as TaskSource,
|
||||
sourceId: null,
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-10'),
|
||||
completedAt: null,
|
||||
dueDate: null,
|
||||
description: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [
|
||||
{
|
||||
tag: {
|
||||
name: 'important',
|
||||
color: '#ff0000',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mockTags = [{ name: 'important', color: '#ff0000' }];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
vi.mocked(prisma.tag.findMany).mockResolvedValue(mockTags as any);
|
||||
|
||||
const result =
|
||||
await TagAnalyticsService.getTagDistributionMetrics(userId);
|
||||
|
||||
expect(result.tagTrends.length).toBeGreaterThan(0);
|
||||
const trend = result.tagTrends.find((t) => t.tagName === 'important');
|
||||
expect(trend).toBeDefined();
|
||||
expect(trend?.count).toBe(1);
|
||||
});
|
||||
|
||||
it('devrait calculer les statistiques des tags', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche 1',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium' as TaskPriority,
|
||||
source: 'manual' as TaskSource,
|
||||
sourceId: null,
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-10'),
|
||||
completedAt: null,
|
||||
dueDate: null,
|
||||
description: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [
|
||||
{
|
||||
tag: {
|
||||
name: 'important',
|
||||
color: '#ff0000',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tâche 2',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium' as TaskPriority,
|
||||
source: 'manual' as TaskSource,
|
||||
sourceId: null,
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-11'),
|
||||
updatedAt: new Date('2024-01-11'),
|
||||
completedAt: null,
|
||||
dueDate: null,
|
||||
description: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [
|
||||
{
|
||||
tag: {
|
||||
name: 'important',
|
||||
color: '#ff0000',
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: {
|
||||
name: 'urgent',
|
||||
color: '#00ff00',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mockTags = [
|
||||
{ name: 'important', color: '#ff0000' },
|
||||
{ name: 'urgent', color: '#00ff00' },
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
vi.mocked(prisma.tag.findMany).mockResolvedValue(mockTags as any);
|
||||
|
||||
const result =
|
||||
await TagAnalyticsService.getTagDistributionMetrics(userId);
|
||||
|
||||
expect(result.tagStats.totalTags).toBe(2);
|
||||
expect(result.tagStats.activeTags).toBe(2);
|
||||
expect(result.tagStats.mostUsedTag).toBe('important');
|
||||
expect(result.tagStats.avgTasksPerTag).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('devrait filtrer par sources si spécifié', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche Jira',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium' as TaskPriority,
|
||||
source: 'jira' as TaskSource,
|
||||
sourceId: 'JIRA-1',
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-10'),
|
||||
completedAt: null,
|
||||
dueDate: null,
|
||||
description: null,
|
||||
jiraProject: 'PROJ',
|
||||
jiraKey: 'JIRA-1',
|
||||
jiraType: 'Task',
|
||||
assignee: null,
|
||||
taskTags: [
|
||||
{
|
||||
tag: {
|
||||
name: 'important',
|
||||
color: '#ff0000',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tâche Manual',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium' as TaskPriority,
|
||||
source: 'manual' as TaskSource,
|
||||
sourceId: null,
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-11'),
|
||||
updatedAt: new Date('2024-01-11'),
|
||||
completedAt: null,
|
||||
dueDate: null,
|
||||
description: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [
|
||||
{
|
||||
tag: {
|
||||
name: 'urgent',
|
||||
color: '#00ff00',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mockTags = [
|
||||
{ name: 'important', color: '#ff0000' },
|
||||
{ name: 'urgent', color: '#00ff00' },
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
vi.mocked(prisma.tag.findMany).mockResolvedValue(mockTags as any);
|
||||
|
||||
const result = await TagAnalyticsService.getTagDistributionMetrics(
|
||||
userId,
|
||||
undefined,
|
||||
['jira']
|
||||
);
|
||||
|
||||
const tagNames = result.tagDistribution.map((t) => t.tagName);
|
||||
expect(tagNames).toContain('important');
|
||||
expect(tagNames).not.toContain('urgent');
|
||||
});
|
||||
|
||||
it('devrait utiliser une période personnalisée si fournie', async () => {
|
||||
const startDate = new Date('2024-01-01');
|
||||
const endDate = new Date('2024-01-10');
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue([]);
|
||||
vi.mocked(prisma.tag.findMany).mockResolvedValue([]);
|
||||
|
||||
await TagAnalyticsService.getTagDistributionMetrics(userId, {
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
});
|
||||
|
||||
expect(prisma.task.findMany).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs de base de données', async () => {
|
||||
vi.mocked(prisma.task.findMany).mockRejectedValue(
|
||||
new Error('Database error')
|
||||
);
|
||||
vi.mocked(prisma.tag.findMany).mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
TagAnalyticsService.getTagDistributionMetrics(userId)
|
||||
).rejects.toThrow(
|
||||
'Impossible de calculer les métriques de distribution par tags'
|
||||
);
|
||||
});
|
||||
|
||||
it('devrait trier les tags par utilisation décroissante', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tâche',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium' as TaskPriority,
|
||||
source: 'manual' as TaskSource,
|
||||
sourceId: null,
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-10'),
|
||||
completedAt: null,
|
||||
dueDate: null,
|
||||
description: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [
|
||||
{
|
||||
tag: {
|
||||
name: 'frequent',
|
||||
color: '#0000ff',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tâche',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium' as TaskPriority,
|
||||
source: 'manual' as TaskSource,
|
||||
sourceId: null,
|
||||
ownerId: userId,
|
||||
createdAt: new Date('2024-01-11'),
|
||||
updatedAt: new Date('2024-01-11'),
|
||||
completedAt: null,
|
||||
dueDate: null,
|
||||
description: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
assignee: null,
|
||||
taskTags: [
|
||||
{
|
||||
tag: {
|
||||
name: 'frequent',
|
||||
color: '#0000ff',
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: {
|
||||
name: 'rare',
|
||||
color: '#ffff00',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mockTags = [
|
||||
{ name: 'frequent', color: '#0000ff' },
|
||||
{ name: 'rare', color: '#ffff00' },
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
vi.mocked(prisma.tag.findMany).mockResolvedValue(mockTags as any);
|
||||
|
||||
const result =
|
||||
await TagAnalyticsService.getTagDistributionMetrics(userId);
|
||||
|
||||
expect(result.tagDistribution[0].tagName).toBe('frequent');
|
||||
expect(result.tagDistribution[0].count).toBeGreaterThan(
|
||||
result.tagDistribution[1]?.count || 0
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -254,12 +254,16 @@ export class ManagerSummaryService {
|
||||
}
|
||||
|
||||
// Compter TOUS les todos associés à cette tâche (pas seulement ceux de la période)
|
||||
// Exclure les todos de type "meeting" (réunion)
|
||||
// car l'accomplissement c'est la tâche complétée, pas seulement les todos de la période
|
||||
const allRelatedTodos = await prisma.dailyCheckbox.count({
|
||||
where: {
|
||||
task: {
|
||||
id: task.id,
|
||||
},
|
||||
type: {
|
||||
not: 'meeting', // Exclure les réunions
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -278,18 +282,19 @@ export class ManagerSummaryService {
|
||||
|
||||
// AJOUTER les todos standalone avec la nouvelle règle de priorité
|
||||
// Exclure les todos déjà comptés dans les tâches complétées
|
||||
// Exclure les todos de type "meeting" (réunion)
|
||||
const standaloneTodos = checkboxes.filter(
|
||||
(checkbox) => !checkbox.task // Todos non liés à une tâche
|
||||
(checkbox) => !checkbox.task && checkbox.type !== 'meeting' // Todos non liés à une tâche et pas de type meeting
|
||||
);
|
||||
|
||||
standaloneTodos.forEach((todo) => {
|
||||
// Appliquer la nouvelle règle de priorité :
|
||||
// Si pas de tâche associée, priorité faible (même pour les meetings)
|
||||
// Si pas de tâche associée, priorité faible
|
||||
const impact: 'high' | 'medium' | 'low' = 'low';
|
||||
|
||||
accomplishments.push({
|
||||
id: `todo-${todo.id}`,
|
||||
title: todo.type === 'meeting' ? `📅 ${todo.text}` : todo.text,
|
||||
title: todo.text,
|
||||
tags: [], // Todos standalone n'ont pas de tags par défaut
|
||||
impact,
|
||||
completedAt: todo.date,
|
||||
|
||||
93
src/services/core/__tests__/database.test.ts
Normal file
93
src/services/core/__tests__/database.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Tests unitaires pour database.ts
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import {
|
||||
testDatabaseConnection,
|
||||
closeDatabaseConnection,
|
||||
clearDatabase,
|
||||
prisma,
|
||||
} from '../database';
|
||||
|
||||
describe('database utilities', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('testDatabaseConnection', () => {
|
||||
it('devrait retourner true si la connexion réussit', async () => {
|
||||
vi.spyOn(prisma, '$connect').mockResolvedValue(undefined);
|
||||
|
||||
const result = await testDatabaseConnection();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(prisma.$connect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait retourner false si la connexion échoue', async () => {
|
||||
vi.spyOn(prisma, '$connect').mockRejectedValue(
|
||||
new Error('Connection failed')
|
||||
);
|
||||
|
||||
const result = await testDatabaseConnection();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('closeDatabaseConnection', () => {
|
||||
it('devrait fermer la connexion avec succès', async () => {
|
||||
vi.spyOn(prisma, '$disconnect').mockResolvedValue(undefined);
|
||||
|
||||
await closeDatabaseConnection();
|
||||
|
||||
expect(prisma.$disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs lors de la fermeture', async () => {
|
||||
vi.spyOn(prisma, '$disconnect').mockRejectedValue(
|
||||
new Error('Disconnect failed')
|
||||
);
|
||||
|
||||
// Ne devrait pas throw
|
||||
await expect(closeDatabaseConnection()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearDatabase', () => {
|
||||
beforeEach(() => {
|
||||
// S'assurer qu'on est pas en production
|
||||
vi.stubEnv('NODE_ENV', 'test');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('devrait nettoyer toutes les tables', async () => {
|
||||
const deleteManySpy = vi.fn().mockResolvedValue({ count: 0 });
|
||||
|
||||
// Mock des méthodes deleteMany directement
|
||||
(prisma.taskTag as any) = { deleteMany: deleteManySpy };
|
||||
(prisma.task as any) = { deleteMany: deleteManySpy };
|
||||
(prisma.tag as any) = { deleteMany: deleteManySpy };
|
||||
(prisma.syncLog as any) = { deleteMany: deleteManySpy };
|
||||
|
||||
await clearDatabase();
|
||||
|
||||
expect(deleteManySpy).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it('ne devrait pas permettre le nettoyage en production', async () => {
|
||||
vi.stubEnv('NODE_ENV', 'production');
|
||||
|
||||
await expect(clearDatabase()).rejects.toThrow(
|
||||
'Cannot clear database in production'
|
||||
);
|
||||
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
});
|
||||
});
|
||||
256
src/services/core/__tests__/system-info.test.ts
Normal file
256
src/services/core/__tests__/system-info.test.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Tests unitaires pour SystemInfoService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { SystemInfoService } from '../system-info';
|
||||
import { prisma } from '../database';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { stat } from 'fs/promises';
|
||||
|
||||
// Mock de prisma
|
||||
vi.mock('../database', () => ({
|
||||
prisma: {
|
||||
task: {
|
||||
count: vi.fn(),
|
||||
},
|
||||
userPreferences: {
|
||||
count: vi.fn(),
|
||||
},
|
||||
tag: {
|
||||
count: vi.fn(),
|
||||
},
|
||||
dailyCheckbox: {
|
||||
count: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock de fs/promises
|
||||
vi.mock('fs/promises', () => ({
|
||||
readFile: vi.fn(),
|
||||
stat: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock de path
|
||||
vi.mock('path', () => ({
|
||||
join: (...args: string[]) => args.join('/'),
|
||||
resolve: (...args: string[]) => args.join('/'),
|
||||
}));
|
||||
|
||||
// Mock du service backup
|
||||
vi.mock('@/services/data-management/backup', () => ({
|
||||
backupService: {
|
||||
listBackups: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('SystemInfoService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.stubEnv('NODE_ENV', 'test');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
describe('getSystemInfo', () => {
|
||||
it('devrait retourner les informations système complètes', async () => {
|
||||
const mockPackageJson = {
|
||||
name: 'towercontrol',
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
vi.mocked(readFile).mockResolvedValue(JSON.stringify(mockPackageJson));
|
||||
vi.mocked(prisma.task.count).mockResolvedValue(10);
|
||||
vi.mocked(prisma.userPreferences.count).mockResolvedValue(1);
|
||||
vi.mocked(prisma.tag.count).mockResolvedValue(5);
|
||||
vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(20);
|
||||
|
||||
const { backupService } = await import(
|
||||
'@/services/data-management/backup'
|
||||
);
|
||||
vi.mocked(backupService.listBackups).mockResolvedValue([
|
||||
{ id: '1', filename: 'backup1.db.gz' },
|
||||
{ id: '2', filename: 'backup2.db.gz' },
|
||||
] as any);
|
||||
|
||||
vi.mocked(stat).mockResolvedValue({
|
||||
size: 1024 * 1024, // 1 MB
|
||||
} as any);
|
||||
|
||||
const result = await SystemInfoService.getSystemInfo();
|
||||
|
||||
expect(result).toHaveProperty('version');
|
||||
expect(result).toHaveProperty('environment');
|
||||
expect(result).toHaveProperty('database');
|
||||
expect(result).toHaveProperty('backups');
|
||||
expect(result).toHaveProperty('app');
|
||||
expect(result).toHaveProperty('uptime');
|
||||
expect(result).toHaveProperty('lastUpdate');
|
||||
|
||||
expect(result.version).toBe('1.0.0');
|
||||
expect(result.environment).toBe('test');
|
||||
expect(result.database.totalTasks).toBe(10);
|
||||
expect(result.database.totalUsers).toBe(1);
|
||||
expect(result.database.totalBackups).toBe(2);
|
||||
expect(result.database.totalTags).toBe(5);
|
||||
expect(result.database.totalDailies).toBe(20);
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs lors de la lecture du package.json', async () => {
|
||||
vi.mocked(readFile).mockRejectedValue(new Error('File not found'));
|
||||
|
||||
vi.mocked(prisma.task.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.userPreferences.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.tag.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0);
|
||||
|
||||
const { backupService } = await import(
|
||||
'@/services/data-management/backup'
|
||||
);
|
||||
vi.mocked(backupService.listBackups).mockResolvedValue([]);
|
||||
|
||||
vi.mocked(stat).mockRejectedValue(new Error('File not found'));
|
||||
|
||||
const result = await SystemInfoService.getSystemInfo();
|
||||
|
||||
expect(result.version).toBe('1.0.0'); // Valeur par défaut
|
||||
expect(result.database.databaseSize).toBe('N/A');
|
||||
});
|
||||
|
||||
it('devrait calculer correctement la taille de la base de données', async () => {
|
||||
const mockPackageJson = {
|
||||
name: 'towercontrol',
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
vi.mocked(readFile).mockResolvedValue(JSON.stringify(mockPackageJson));
|
||||
vi.mocked(prisma.task.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.userPreferences.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.tag.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0);
|
||||
|
||||
const { backupService } = await import(
|
||||
'@/services/data-management/backup'
|
||||
);
|
||||
vi.mocked(backupService.listBackups).mockResolvedValue([]);
|
||||
|
||||
// Test différentes tailles
|
||||
vi.mocked(stat).mockResolvedValue({
|
||||
size: 1024, // 1 KB
|
||||
} as any);
|
||||
|
||||
const result = await SystemInfoService.getSystemInfo();
|
||||
|
||||
expect(result.database.databaseSize).toContain('KB');
|
||||
});
|
||||
|
||||
it('devrait utiliser DATABASE_URL pour trouver la base de données', async () => {
|
||||
vi.stubEnv('DATABASE_URL', 'file:/path/to/db.db');
|
||||
|
||||
const mockPackageJson = {
|
||||
name: 'towercontrol',
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
vi.mocked(readFile).mockResolvedValue(JSON.stringify(mockPackageJson));
|
||||
vi.mocked(prisma.task.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.userPreferences.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.tag.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0);
|
||||
|
||||
const { backupService } = await import(
|
||||
'@/services/data-management/backup'
|
||||
);
|
||||
vi.mocked(backupService.listBackups).mockResolvedValue([]);
|
||||
|
||||
vi.mocked(stat).mockResolvedValue({
|
||||
size: 1024,
|
||||
} as any);
|
||||
|
||||
await SystemInfoService.getSystemInfo();
|
||||
|
||||
expect(stat).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait utiliser BACKUP_DATABASE_PATH si défini', async () => {
|
||||
vi.stubEnv('BACKUP_DATABASE_PATH', '/custom/path/db.db');
|
||||
|
||||
const mockPackageJson = {
|
||||
name: 'towercontrol',
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
vi.mocked(readFile).mockResolvedValue(JSON.stringify(mockPackageJson));
|
||||
vi.mocked(prisma.task.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.userPreferences.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.tag.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0);
|
||||
|
||||
const { backupService } = await import(
|
||||
'@/services/data-management/backup'
|
||||
);
|
||||
vi.mocked(backupService.listBackups).mockResolvedValue([]);
|
||||
|
||||
vi.mocked(stat).mockResolvedValue({
|
||||
size: 1024,
|
||||
} as any);
|
||||
|
||||
await SystemInfoService.getSystemInfo();
|
||||
|
||||
expect(stat).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs de base de données', async () => {
|
||||
const mockPackageJson = {
|
||||
name: 'towercontrol',
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
vi.mocked(readFile).mockResolvedValue(JSON.stringify(mockPackageJson));
|
||||
vi.mocked(prisma.task.count).mockRejectedValue(
|
||||
new Error('Database error')
|
||||
);
|
||||
|
||||
const { backupService } = await import(
|
||||
'@/services/data-management/backup'
|
||||
);
|
||||
vi.mocked(backupService.listBackups).mockResolvedValue([]);
|
||||
|
||||
const result = await SystemInfoService.getSystemInfo();
|
||||
|
||||
expect(result.database.totalTasks).toBe(0);
|
||||
expect(result.database.totalUsers).toBe(0);
|
||||
});
|
||||
|
||||
it("devrait calculer correctement l'uptime", async () => {
|
||||
const mockPackageJson = {
|
||||
name: 'towercontrol',
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
vi.mocked(readFile).mockResolvedValue(JSON.stringify(mockPackageJson));
|
||||
vi.mocked(prisma.task.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.userPreferences.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.tag.count).mockResolvedValue(0);
|
||||
vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0);
|
||||
|
||||
const { backupService } = await import(
|
||||
'@/services/data-management/backup'
|
||||
);
|
||||
vi.mocked(backupService.listBackups).mockResolvedValue([]);
|
||||
|
||||
vi.mocked(stat).mockResolvedValue({
|
||||
size: 1024,
|
||||
} as any);
|
||||
|
||||
const result = await SystemInfoService.getSystemInfo();
|
||||
|
||||
expect(result.uptime).toBeDefined();
|
||||
expect(result.uptime.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
836
src/services/core/__tests__/user-preferences.test.ts
Normal file
836
src/services/core/__tests__/user-preferences.test.ts
Normal file
@@ -0,0 +1,836 @@
|
||||
/**
|
||||
* Tests unitaires pour UserPreferencesService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { userPreferencesService } from '../user-preferences';
|
||||
import { prisma } from '../database';
|
||||
import { getConfig } from '@/lib/config';
|
||||
import {
|
||||
KanbanFilters,
|
||||
ViewPreferences,
|
||||
ColumnVisibility,
|
||||
JiraConfig,
|
||||
} from '@/lib/types';
|
||||
import { TfsConfig } from '@/services/integrations/tfs/tfs';
|
||||
|
||||
// Mock de prisma
|
||||
vi.mock('../database', () => ({
|
||||
prisma: {
|
||||
user: {
|
||||
upsert: vi.fn(),
|
||||
},
|
||||
userPreferences: {
|
||||
upsert: vi.fn(),
|
||||
update: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
$executeRaw: vi.fn(),
|
||||
$queryRaw: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock de getConfig
|
||||
vi.mock('@/lib/config', () => ({
|
||||
getConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('UserPreferencesService', () => {
|
||||
const userId = 'test-user-id';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Kanban Filters', () => {
|
||||
it('devrait sauvegarder les filtres Kanban', async () => {
|
||||
const filters: KanbanFilters = {
|
||||
search: 'test',
|
||||
tags: ['important'],
|
||||
priorities: ['high'],
|
||||
showCompleted: false,
|
||||
sortBy: 'priority',
|
||||
showWithDueDate: true,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: filters,
|
||||
viewPreferences: {},
|
||||
columnVisibility: {},
|
||||
jiraConfig: {},
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.update).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: filters,
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
await userPreferencesService.saveKanbanFilters(userId, filters);
|
||||
|
||||
expect(prisma.userPreferences.update).toHaveBeenCalledWith({
|
||||
where: { userId },
|
||||
data: { kanbanFilters: filters },
|
||||
});
|
||||
});
|
||||
|
||||
it('devrait récupérer les filtres Kanban avec valeurs par défaut', async () => {
|
||||
const filters: KanbanFilters = {
|
||||
search: 'test',
|
||||
tags: ['important'],
|
||||
priorities: [],
|
||||
showCompleted: true,
|
||||
sortBy: '',
|
||||
showWithDueDate: false,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: filters,
|
||||
viewPreferences: {},
|
||||
columnVisibility: {},
|
||||
jiraConfig: {},
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
const result = await userPreferencesService.getKanbanFilters(userId);
|
||||
|
||||
expect(result).toHaveProperty('search');
|
||||
expect(result).toHaveProperty('tags');
|
||||
expect(result).toHaveProperty('priorities');
|
||||
expect(result).toHaveProperty('showCompleted');
|
||||
});
|
||||
|
||||
it("devrait retourner les valeurs par défaut en cas d'erreur", async () => {
|
||||
vi.mocked(prisma.user.upsert).mockRejectedValue(
|
||||
new Error('Database error')
|
||||
);
|
||||
|
||||
const result = await userPreferencesService.getKanbanFilters(userId);
|
||||
|
||||
expect(result).toHaveProperty('search');
|
||||
expect(result.search).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('View Preferences', () => {
|
||||
it('devrait sauvegarder les préférences de vue', async () => {
|
||||
const preferences: ViewPreferences = {
|
||||
compactView: true,
|
||||
swimlanesByTags: true,
|
||||
swimlanesMode: 'tags',
|
||||
showObjectives: false,
|
||||
showFilters: true,
|
||||
objectivesCollapsed: false,
|
||||
theme: 'light',
|
||||
fontSize: 'large',
|
||||
backgroundImage: undefined,
|
||||
backgroundBlur: 0,
|
||||
backgroundOpacity: 100,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: preferences,
|
||||
columnVisibility: {},
|
||||
jiraConfig: {},
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.update).mockResolvedValue({
|
||||
userId,
|
||||
viewPreferences: preferences,
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
await userPreferencesService.saveViewPreferences(userId, preferences);
|
||||
|
||||
expect(prisma.userPreferences.update).toHaveBeenCalledWith({
|
||||
where: { userId },
|
||||
data: { viewPreferences: preferences },
|
||||
});
|
||||
});
|
||||
|
||||
it('devrait récupérer le thème', async () => {
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: { theme: 'light' },
|
||||
columnVisibility: {},
|
||||
jiraConfig: {},
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
const theme = await userPreferencesService.getTheme(userId);
|
||||
|
||||
expect(theme).toBe('light');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Column Visibility', () => {
|
||||
it('devrait sauvegarder la visibilité des colonnes', async () => {
|
||||
const visibility: ColumnVisibility = {
|
||||
hiddenStatuses: ['archived', 'cancelled'],
|
||||
};
|
||||
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: {},
|
||||
columnVisibility: visibility,
|
||||
jiraConfig: {},
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.update).mockResolvedValue({
|
||||
userId,
|
||||
columnVisibility: visibility,
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
await userPreferencesService.saveColumnVisibility(userId, visibility);
|
||||
|
||||
expect(prisma.userPreferences.update).toHaveBeenCalledWith({
|
||||
where: { userId },
|
||||
data: { columnVisibility: visibility },
|
||||
});
|
||||
});
|
||||
|
||||
it("devrait toggle la visibilité d'une colonne", async () => {
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: {},
|
||||
columnVisibility: { hiddenStatuses: [] },
|
||||
jiraConfig: {},
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.update).mockResolvedValue({
|
||||
userId,
|
||||
columnVisibility: { hiddenStatuses: ['archived'] },
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
await userPreferencesService.toggleColumnVisibility(userId, 'archived');
|
||||
|
||||
expect(prisma.userPreferences.update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Jira Config', () => {
|
||||
it('devrait sauvegarder la configuration Jira', async () => {
|
||||
const config: JiraConfig = {
|
||||
enabled: true,
|
||||
baseUrl: 'https://jira.example.com',
|
||||
email: 'user@example.com',
|
||||
apiToken: 'token123',
|
||||
ignoredProjects: [],
|
||||
};
|
||||
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: {},
|
||||
columnVisibility: {},
|
||||
jiraConfig: config,
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.update).mockResolvedValue({
|
||||
userId,
|
||||
jiraConfig: config,
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
await userPreferencesService.saveJiraConfig(userId, config);
|
||||
|
||||
expect(prisma.userPreferences.update).toHaveBeenCalledWith({
|
||||
where: { userId },
|
||||
data: { jiraConfig: config },
|
||||
});
|
||||
});
|
||||
|
||||
it('devrait récupérer la configuration Jira depuis la DB', async () => {
|
||||
const config: JiraConfig = {
|
||||
enabled: true,
|
||||
baseUrl: 'https://jira.example.com',
|
||||
email: 'user@example.com',
|
||||
apiToken: 'token123',
|
||||
ignoredProjects: [],
|
||||
};
|
||||
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: {},
|
||||
columnVisibility: {},
|
||||
jiraConfig: config,
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
const result = await userPreferencesService.getJiraConfig(userId);
|
||||
|
||||
expect(result.baseUrl).toBe('https://jira.example.com');
|
||||
expect(result.email).toBe('user@example.com');
|
||||
});
|
||||
|
||||
it("devrait utiliser les variables d'environnement si pas de config DB", async () => {
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: {},
|
||||
columnVisibility: {},
|
||||
jiraConfig: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
vi.mocked(getConfig).mockReturnValue({
|
||||
integrations: {
|
||||
jira: {
|
||||
baseUrl: 'https://env-jira.example.com',
|
||||
email: 'env@example.com',
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
const result = await userPreferencesService.getJiraConfig(userId);
|
||||
|
||||
expect(result.baseUrl).toBe('https://env-jira.example.com');
|
||||
expect(result.email).toBe('env@example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TFS Config', () => {
|
||||
it('devrait sauvegarder la configuration TFS', async () => {
|
||||
const config: TfsConfig = {
|
||||
enabled: true,
|
||||
organizationUrl: 'https://dev.azure.com/org',
|
||||
projectName: 'MyProject',
|
||||
personalAccessToken: 'token123',
|
||||
repositories: ['repo1', 'repo2'],
|
||||
ignoredRepositories: [],
|
||||
maxSyncPeriod: '90d',
|
||||
};
|
||||
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: {},
|
||||
columnVisibility: {},
|
||||
jiraConfig: {},
|
||||
tfsConfig: config,
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.update).mockResolvedValue({
|
||||
userId,
|
||||
tfsConfig: config,
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
await userPreferencesService.saveTfsConfig(userId, config);
|
||||
|
||||
expect(prisma.userPreferences.update).toHaveBeenCalledWith({
|
||||
where: { userId },
|
||||
data: { tfsConfig: config },
|
||||
});
|
||||
});
|
||||
|
||||
it('devrait récupérer la configuration TFS', async () => {
|
||||
const config: TfsConfig = {
|
||||
enabled: true,
|
||||
organizationUrl: 'https://dev.azure.com/org',
|
||||
projectName: 'MyProject',
|
||||
personalAccessToken: 'token123',
|
||||
repositories: [],
|
||||
ignoredRepositories: [],
|
||||
maxSyncPeriod: '90d',
|
||||
};
|
||||
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: {},
|
||||
columnVisibility: {},
|
||||
jiraConfig: {},
|
||||
tfsConfig: config,
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
const result = await userPreferencesService.getTfsConfig(userId);
|
||||
|
||||
expect(result.organizationUrl).toBe('https://dev.azure.com/org');
|
||||
expect(result.projectName).toBe('MyProject');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scheduler Config', () => {
|
||||
it('devrait sauvegarder la configuration scheduler Jira', async () => {
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: {},
|
||||
columnVisibility: {},
|
||||
jiraConfig: {},
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
await userPreferencesService.saveJiraSchedulerConfig(
|
||||
userId,
|
||||
true,
|
||||
'hourly'
|
||||
);
|
||||
|
||||
expect(prisma.$executeRaw).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait récupérer la configuration scheduler Jira', async () => {
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: {},
|
||||
columnVisibility: {},
|
||||
jiraConfig: {},
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
vi.mocked(prisma.$queryRaw).mockResolvedValue([
|
||||
{ jiraAutoSync: 1, jiraSyncInterval: 'hourly' },
|
||||
] as any);
|
||||
|
||||
const result =
|
||||
await userPreferencesService.getJiraSchedulerConfig(userId);
|
||||
|
||||
expect(result.jiraAutoSync).toBe(true);
|
||||
expect(result.jiraSyncInterval).toBe('hourly');
|
||||
});
|
||||
|
||||
it('devrait sauvegarder la configuration scheduler TFS', async () => {
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: {},
|
||||
columnVisibility: {},
|
||||
jiraConfig: {},
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
await userPreferencesService.saveTfsSchedulerConfig(
|
||||
userId,
|
||||
true,
|
||||
'daily'
|
||||
);
|
||||
|
||||
expect(prisma.$executeRaw).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait récupérer la configuration scheduler TFS', async () => {
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: {},
|
||||
columnVisibility: {},
|
||||
jiraConfig: {},
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
vi.mocked(prisma.$queryRaw).mockResolvedValue([
|
||||
{ tfsAutoSync: 1, tfsSyncInterval: 'daily' },
|
||||
] as any);
|
||||
|
||||
const result = await userPreferencesService.getTfsSchedulerConfig(userId);
|
||||
|
||||
expect(result.tfsAutoSync).toBe(true);
|
||||
expect(result.tfsSyncInterval).toBe('daily');
|
||||
});
|
||||
});
|
||||
|
||||
describe('All Preferences', () => {
|
||||
it('devrait récupérer toutes les préférences', async () => {
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: {},
|
||||
columnVisibility: {},
|
||||
jiraConfig: {},
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
vi.mocked(prisma.$queryRaw).mockResolvedValue([
|
||||
{ jiraAutoSync: 0, jiraSyncInterval: 'daily' },
|
||||
{ tfsAutoSync: 0, tfsSyncInterval: 'daily' },
|
||||
] as any);
|
||||
|
||||
vi.mocked(getConfig).mockReturnValue({
|
||||
integrations: {
|
||||
jira: {
|
||||
baseUrl: '',
|
||||
email: '',
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
const result = await userPreferencesService.getAllPreferences(userId);
|
||||
|
||||
expect(result).toHaveProperty('kanbanFilters');
|
||||
expect(result).toHaveProperty('viewPreferences');
|
||||
expect(result).toHaveProperty('columnVisibility');
|
||||
expect(result).toHaveProperty('jiraConfig');
|
||||
expect(result).toHaveProperty('jiraAutoSync');
|
||||
expect(result).toHaveProperty('jiraSyncInterval');
|
||||
expect(result).toHaveProperty('tfsConfig');
|
||||
expect(result).toHaveProperty('tfsAutoSync');
|
||||
expect(result).toHaveProperty('tfsSyncInterval');
|
||||
});
|
||||
|
||||
it('devrait sauvegarder toutes les préférences', async () => {
|
||||
const preferences = {
|
||||
kanbanFilters: {
|
||||
search: '',
|
||||
tags: [],
|
||||
priorities: [],
|
||||
showCompleted: true,
|
||||
sortBy: '',
|
||||
showWithDueDate: false,
|
||||
},
|
||||
viewPreferences: {
|
||||
compactView: false,
|
||||
swimlanesByTags: false,
|
||||
swimlanesMode: 'tags' as const,
|
||||
showObjectives: true,
|
||||
showFilters: true,
|
||||
objectivesCollapsed: false,
|
||||
theme: 'dark' as const,
|
||||
fontSize: 'medium' as const,
|
||||
backgroundImage: undefined,
|
||||
backgroundBlur: 0,
|
||||
backgroundOpacity: 100,
|
||||
},
|
||||
columnVisibility: {
|
||||
hiddenStatuses: [],
|
||||
},
|
||||
jiraConfig: {
|
||||
enabled: false,
|
||||
baseUrl: '',
|
||||
email: '',
|
||||
apiToken: '',
|
||||
ignoredProjects: [],
|
||||
},
|
||||
jiraAutoSync: false,
|
||||
jiraSyncInterval: 'daily' as const,
|
||||
tfsConfig: {
|
||||
enabled: false,
|
||||
organizationUrl: '',
|
||||
projectName: '',
|
||||
personalAccessToken: '',
|
||||
repositories: [],
|
||||
ignoredRepositories: [],
|
||||
maxSyncPeriod: '90d' as const,
|
||||
},
|
||||
tfsAutoSync: false,
|
||||
tfsSyncInterval: 'daily' as const,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: {},
|
||||
columnVisibility: {},
|
||||
jiraConfig: {},
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.update).mockResolvedValue({
|
||||
userId,
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
await userPreferencesService.saveAllPreferences(userId, preferences);
|
||||
|
||||
expect(prisma.userPreferences.update).toHaveBeenCalled();
|
||||
expect(prisma.$executeRaw).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait remettre à zéro toutes les préférences', async () => {
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: {},
|
||||
columnVisibility: {},
|
||||
jiraConfig: {},
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.update).mockResolvedValue({
|
||||
userId,
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
await userPreferencesService.resetAllPreferences(userId);
|
||||
|
||||
expect(prisma.userPreferences.update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Update methods', () => {
|
||||
it('devrait mettre à jour partiellement les filtres Kanban', async () => {
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: { search: '', tags: [] },
|
||||
viewPreferences: {},
|
||||
columnVisibility: {},
|
||||
jiraConfig: {},
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.update).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: { search: 'new', tags: [] },
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
await userPreferencesService.updateKanbanFilters(userId, {
|
||||
search: 'new',
|
||||
});
|
||||
|
||||
expect(prisma.userPreferences.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait mettre à jour partiellement les préférences de vue', async () => {
|
||||
vi.mocked(prisma.user.upsert).mockResolvedValue({
|
||||
id: userId,
|
||||
email: `${userId}@towercontrol.local`,
|
||||
name: 'Default User',
|
||||
role: 'user',
|
||||
password: 'default',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId,
|
||||
kanbanFilters: {},
|
||||
viewPreferences: { theme: 'dark' },
|
||||
columnVisibility: {},
|
||||
jiraConfig: {},
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.userPreferences.update).mockResolvedValue({
|
||||
userId,
|
||||
viewPreferences: { theme: 'light' },
|
||||
} as any);
|
||||
|
||||
vi.mocked(prisma.$executeRaw).mockResolvedValue(0);
|
||||
|
||||
await userPreferencesService.updateViewPreferences(userId, {
|
||||
theme: 'light',
|
||||
});
|
||||
|
||||
expect(prisma.userPreferences.update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
319
src/services/data-management/__tests__/backup-scheduler.test.ts
Normal file
319
src/services/data-management/__tests__/backup-scheduler.test.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* Tests unitaires pour BackupScheduler
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { BackupScheduler } from '../backup-scheduler';
|
||||
import { backupService } from '../backup';
|
||||
import { getToday, addMinutes } from '@/lib/date-utils';
|
||||
|
||||
// Mock de backupService
|
||||
vi.mock('../backup', () => ({
|
||||
backupService: {
|
||||
getConfigSync: vi.fn(),
|
||||
createBackup: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock de date-utils
|
||||
vi.mock('@/lib/date-utils', () => ({
|
||||
getToday: vi.fn(),
|
||||
addMinutes: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('BackupScheduler', () => {
|
||||
let scheduler: BackupScheduler;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
scheduler = new BackupScheduler();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
scheduler.stop();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
it('devrait démarrer le scheduler si activé', () => {
|
||||
vi.mocked(backupService.getConfigSync).mockReturnValue({
|
||||
enabled: true,
|
||||
interval: 'hourly',
|
||||
maxBackups: 5,
|
||||
backupPath: '/backups',
|
||||
} as any);
|
||||
|
||||
scheduler.start();
|
||||
|
||||
expect(scheduler.isActive()).toBe(true);
|
||||
});
|
||||
|
||||
it('ne devrait pas démarrer si désactivé', () => {
|
||||
vi.mocked(backupService.getConfigSync).mockReturnValue({
|
||||
enabled: false,
|
||||
interval: 'hourly',
|
||||
maxBackups: 5,
|
||||
backupPath: '/backups',
|
||||
} as any);
|
||||
|
||||
scheduler.start();
|
||||
|
||||
expect(scheduler.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('ne devrait pas démarrer deux fois', () => {
|
||||
vi.mocked(backupService.getConfigSync).mockReturnValue({
|
||||
enabled: true,
|
||||
interval: 'hourly',
|
||||
maxBackups: 5,
|
||||
backupPath: '/backups',
|
||||
} as any);
|
||||
|
||||
scheduler.start();
|
||||
const firstCall = scheduler.isActive();
|
||||
scheduler.start();
|
||||
const secondCall = scheduler.isActive();
|
||||
|
||||
expect(firstCall).toBe(true);
|
||||
expect(secondCall).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stop', () => {
|
||||
it('devrait arrêter le scheduler', () => {
|
||||
vi.mocked(backupService.getConfigSync).mockReturnValue({
|
||||
enabled: true,
|
||||
interval: 'hourly',
|
||||
maxBackups: 5,
|
||||
backupPath: '/backups',
|
||||
} as any);
|
||||
|
||||
scheduler.start();
|
||||
expect(scheduler.isActive()).toBe(true);
|
||||
|
||||
scheduler.stop();
|
||||
expect(scheduler.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it("devrait gérer l'arrêt si pas démarré", () => {
|
||||
scheduler.stop();
|
||||
expect(scheduler.isActive()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restart', () => {
|
||||
it('devrait redémarrer le scheduler', () => {
|
||||
vi.mocked(backupService.getConfigSync).mockReturnValue({
|
||||
enabled: true,
|
||||
interval: 'hourly',
|
||||
maxBackups: 5,
|
||||
backupPath: '/backups',
|
||||
} as any);
|
||||
|
||||
scheduler.start();
|
||||
expect(scheduler.isActive()).toBe(true);
|
||||
|
||||
scheduler.restart();
|
||||
expect(scheduler.isActive()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isActive', () => {
|
||||
it('devrait retourner false si pas démarré', () => {
|
||||
expect(scheduler.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('devrait retourner true si démarré', () => {
|
||||
vi.mocked(backupService.getConfigSync).mockReturnValue({
|
||||
enabled: true,
|
||||
interval: 'hourly',
|
||||
maxBackups: 5,
|
||||
backupPath: '/backups',
|
||||
} as any);
|
||||
|
||||
scheduler.start();
|
||||
expect(scheduler.isActive()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('performScheduledBackup', () => {
|
||||
it('devrait exécuter une sauvegarde automatique', async () => {
|
||||
vi.mocked(backupService.getConfigSync).mockReturnValue({
|
||||
enabled: true,
|
||||
interval: 'hourly',
|
||||
maxBackups: 5,
|
||||
backupPath: '/backups',
|
||||
} as any);
|
||||
|
||||
vi.mocked(backupService.createBackup).mockResolvedValue({
|
||||
id: '1',
|
||||
filename: 'backup.db.gz',
|
||||
size: 1024,
|
||||
createdAt: new Date(),
|
||||
type: 'automatic',
|
||||
status: 'success',
|
||||
} as any);
|
||||
|
||||
scheduler.start();
|
||||
|
||||
// Avancer le timer pour déclencher la sauvegarde une seule fois
|
||||
vi.advanceTimersByTime(60 * 60 * 1000); // 1 heure
|
||||
|
||||
// Attendre que la sauvegarde soit exécutée
|
||||
await vi.waitFor(() => {
|
||||
expect(backupService.createBackup).toHaveBeenCalledWith('automatic');
|
||||
});
|
||||
|
||||
scheduler.stop();
|
||||
});
|
||||
|
||||
it('devrait gérer les sauvegardes ignorées', async () => {
|
||||
vi.mocked(backupService.getConfigSync).mockReturnValue({
|
||||
enabled: true,
|
||||
interval: 'hourly',
|
||||
maxBackups: 5,
|
||||
backupPath: '/backups',
|
||||
} as any);
|
||||
|
||||
vi.mocked(backupService.createBackup).mockResolvedValue(null);
|
||||
|
||||
scheduler.start();
|
||||
|
||||
vi.advanceTimersByTime(60 * 60 * 1000);
|
||||
await vi.waitFor(() => {
|
||||
expect(backupService.createBackup).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
scheduler.stop();
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs de sauvegarde', async () => {
|
||||
vi.mocked(backupService.getConfigSync).mockReturnValue({
|
||||
enabled: true,
|
||||
interval: 'hourly',
|
||||
maxBackups: 5,
|
||||
backupPath: '/backups',
|
||||
} as any);
|
||||
|
||||
vi.mocked(backupService.createBackup).mockRejectedValue(
|
||||
new Error('Backup failed')
|
||||
);
|
||||
|
||||
scheduler.start();
|
||||
|
||||
vi.advanceTimersByTime(60 * 60 * 1000);
|
||||
await vi.waitFor(() => {
|
||||
expect(backupService.createBackup).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
scheduler.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIntervalMs', () => {
|
||||
it('devrait convertir hourly en millisecondes', () => {
|
||||
vi.mocked(backupService.getConfigSync).mockReturnValue({
|
||||
enabled: true,
|
||||
interval: 'hourly',
|
||||
maxBackups: 5,
|
||||
backupPath: '/backups',
|
||||
} as any);
|
||||
|
||||
scheduler.start();
|
||||
const status = scheduler.getStatus();
|
||||
|
||||
expect(status.interval).toBe('hourly');
|
||||
});
|
||||
|
||||
it('devrait convertir daily en millisecondes', () => {
|
||||
vi.mocked(backupService.getConfigSync).mockReturnValue({
|
||||
enabled: true,
|
||||
interval: 'daily',
|
||||
maxBackups: 5,
|
||||
backupPath: '/backups',
|
||||
} as any);
|
||||
|
||||
scheduler.start();
|
||||
const status = scheduler.getStatus();
|
||||
|
||||
expect(status.interval).toBe('daily');
|
||||
});
|
||||
|
||||
it('devrait convertir weekly en millisecondes', () => {
|
||||
vi.mocked(backupService.getConfigSync).mockReturnValue({
|
||||
enabled: true,
|
||||
interval: 'weekly',
|
||||
maxBackups: 5,
|
||||
backupPath: '/backups',
|
||||
} as any);
|
||||
|
||||
scheduler.start();
|
||||
const status = scheduler.getStatus();
|
||||
|
||||
expect(status.interval).toBe('weekly');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextBackupTime', () => {
|
||||
it('devrait retourner null si pas démarré', () => {
|
||||
const nextTime = scheduler.getNextBackupTime();
|
||||
|
||||
expect(nextTime).toBeNull();
|
||||
});
|
||||
|
||||
it('devrait calculer le prochain moment de sauvegarde', () => {
|
||||
const mockDate = new Date('2024-01-15T12:00:00Z');
|
||||
vi.mocked(getToday).mockReturnValue(mockDate);
|
||||
vi.mocked(addMinutes).mockReturnValue(new Date('2024-01-15T13:00:00Z'));
|
||||
|
||||
vi.mocked(backupService.getConfigSync).mockReturnValue({
|
||||
enabled: true,
|
||||
interval: 'hourly',
|
||||
maxBackups: 5,
|
||||
backupPath: '/backups',
|
||||
} as any);
|
||||
|
||||
scheduler.start();
|
||||
const nextTime = scheduler.getNextBackupTime();
|
||||
|
||||
expect(nextTime).not.toBeNull();
|
||||
expect(addMinutes).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatus', () => {
|
||||
it('devrait retourner le statut du scheduler', () => {
|
||||
vi.mocked(backupService.getConfigSync).mockReturnValue({
|
||||
enabled: true,
|
||||
interval: 'daily',
|
||||
maxBackups: 10,
|
||||
backupPath: '/custom/backups',
|
||||
} as any);
|
||||
|
||||
scheduler.start();
|
||||
const status = scheduler.getStatus();
|
||||
|
||||
expect(status.isRunning).toBe(true);
|
||||
expect(status.isEnabled).toBe(true);
|
||||
expect(status.interval).toBe('daily');
|
||||
expect(status.maxBackups).toBe(10);
|
||||
expect(status.backupPath).toBe('/custom/backups');
|
||||
});
|
||||
|
||||
it('devrait retourner le statut si pas démarré', () => {
|
||||
vi.mocked(backupService.getConfigSync).mockReturnValue({
|
||||
enabled: false,
|
||||
interval: 'hourly',
|
||||
maxBackups: 5,
|
||||
backupPath: '/backups',
|
||||
} as any);
|
||||
|
||||
const status = scheduler.getStatus();
|
||||
|
||||
expect(status.isRunning).toBe(false);
|
||||
expect(status.isEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
400
src/services/data-management/__tests__/backup.test.ts
Normal file
400
src/services/data-management/__tests__/backup.test.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
/**
|
||||
* Tests unitaires pour BackupService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { BackupService } from '../backup';
|
||||
import { prisma } from '@/services/core/database';
|
||||
import { userPreferencesService } from '@/services/core/user-preferences';
|
||||
import { BackupUtils } from '@/lib/backup-utils';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Mock de prisma
|
||||
vi.mock('@/services/core/database', () => ({
|
||||
prisma: {
|
||||
$disconnect: vi.fn(),
|
||||
$connect: vi.fn(),
|
||||
$queryRaw: vi.fn(),
|
||||
userPreferences: {
|
||||
upsert: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock de userPreferencesService
|
||||
vi.mock('@/services/core/user-preferences', () => ({
|
||||
userPreferencesService: {
|
||||
getAllPreferences: vi.fn(),
|
||||
getViewPreferences: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock de BackupUtils
|
||||
vi.mock('@/lib/backup-utils', () => ({
|
||||
BackupUtils: {
|
||||
resolveBackupStoragePath: vi.fn(),
|
||||
resolveDatabasePath: vi.fn(),
|
||||
calculateFileHash: vi.fn(),
|
||||
ensureDirectory: vi.fn(),
|
||||
createSQLiteBackup: vi.fn(),
|
||||
compressFile: vi.fn(),
|
||||
decompressFileTemp: vi.fn(),
|
||||
writeLogEntry: vi.fn(),
|
||||
parseBackupFilename: vi.fn(),
|
||||
generateBackupFilename: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock de fs (pour promises)
|
||||
vi.mock('fs', () => ({
|
||||
promises: {
|
||||
readFile: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
stat: vi.fn(),
|
||||
readdir: vi.fn(),
|
||||
unlink: vi.fn(),
|
||||
access: vi.fn(),
|
||||
copyFile: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('BackupService', () => {
|
||||
let backupService: BackupService;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
backupService = new BackupService();
|
||||
|
||||
// Mocks par défaut
|
||||
vi.mocked(BackupUtils.resolveBackupStoragePath).mockReturnValue('/backups');
|
||||
vi.mocked(BackupUtils.resolveDatabasePath).mockReturnValue('/db/dev.db');
|
||||
vi.mocked(BackupUtils.calculateFileHash).mockResolvedValue('hash123');
|
||||
vi.mocked(BackupUtils.ensureDirectory).mockResolvedValue(undefined);
|
||||
vi.mocked(BackupUtils.createSQLiteBackup).mockResolvedValue(undefined);
|
||||
vi.mocked(BackupUtils.compressFile).mockResolvedValue(
|
||||
'/backups/file.db.gz'
|
||||
);
|
||||
vi.mocked(BackupUtils.parseBackupFilename).mockReturnValue({
|
||||
type: 'manual',
|
||||
date: new Date(),
|
||||
});
|
||||
vi.mocked(BackupUtils.generateBackupFilename).mockReturnValue(
|
||||
'towercontrol_manual_2024-01-01.db'
|
||||
);
|
||||
vi.mocked(fs.stat).mockResolvedValue({
|
||||
size: 1024,
|
||||
birthtime: new Date(),
|
||||
} as any);
|
||||
vi.mocked(fs.readdir).mockResolvedValue([]);
|
||||
vi.mocked(userPreferencesService.getAllPreferences).mockResolvedValue({
|
||||
viewPreferences: {},
|
||||
} as any);
|
||||
vi.mocked(userPreferencesService.getViewPreferences).mockResolvedValue(
|
||||
{} as any
|
||||
);
|
||||
});
|
||||
|
||||
describe('createBackup', () => {
|
||||
it('devrait créer une sauvegarde manuelle', async () => {
|
||||
vi.mocked(BackupUtils.compressFile).mockResolvedValue(
|
||||
'/backups/towercontrol_manual_2024-01-01.db.gz'
|
||||
);
|
||||
vi.mocked(fs.stat).mockResolvedValue({
|
||||
size: 2048,
|
||||
birthtime: new Date(),
|
||||
} as any);
|
||||
|
||||
const result = await backupService.createBackup('manual', true);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.type).toBe('manual');
|
||||
expect(result?.status).toBe('success');
|
||||
expect(result?.filename).toContain('towercontrol_manual');
|
||||
});
|
||||
|
||||
it('devrait créer une sauvegarde automatique', async () => {
|
||||
vi.mocked(BackupUtils.compressFile).mockResolvedValue(
|
||||
'/backups/towercontrol_automatic_2024-01-01.db.gz'
|
||||
);
|
||||
|
||||
const result = await backupService.createBackup('automatic', true);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.type).toBe('automatic');
|
||||
});
|
||||
|
||||
it('devrait retourner null si pas de changements et forceCreate=false', async () => {
|
||||
// Mock pour simuler qu'il n'y a pas de changements
|
||||
vi.mocked(fs.readdir).mockResolvedValue([
|
||||
'towercontrol_manual_2024-01-01.db.gz',
|
||||
] as any);
|
||||
vi.mocked(BackupUtils.calculateFileHash).mockResolvedValue('samehash');
|
||||
vi.mocked(fs.readFile).mockResolvedValue(
|
||||
JSON.stringify({ databaseHash: 'samehash' })
|
||||
);
|
||||
|
||||
const result = await backupService.createBackup('automatic', false);
|
||||
|
||||
// Si pas de changements, devrait retourner null
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs lors de la création', async () => {
|
||||
vi.mocked(BackupUtils.createSQLiteBackup).mockRejectedValue(
|
||||
new Error('Backup failed')
|
||||
);
|
||||
|
||||
const result = await backupService.createBackup('manual', true);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.status).toBe('failed');
|
||||
expect(result?.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('listBackups', () => {
|
||||
it('devrait lister les sauvegardes disponibles', async () => {
|
||||
vi.mocked(fs.readdir).mockResolvedValue([
|
||||
'towercontrol_manual_2024-01-01.db.gz',
|
||||
'towercontrol_automatic_2024-01-02.db.gz',
|
||||
] as any);
|
||||
|
||||
const backups = await backupService.listBackups();
|
||||
|
||||
expect(backups).toHaveLength(2);
|
||||
expect(backups[0].filename).toContain('towercontrol_');
|
||||
});
|
||||
|
||||
it('devrait retourner une liste vide si aucun backup', async () => {
|
||||
vi.mocked(fs.readdir).mockResolvedValue([]);
|
||||
|
||||
const backups = await backupService.listBackups();
|
||||
|
||||
expect(backups).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('devrait filtrer les fichiers non-backup', async () => {
|
||||
vi.mocked(fs.readdir).mockResolvedValue([
|
||||
'towercontrol_manual_2024-01-01.db.gz',
|
||||
'other_file.txt',
|
||||
'backup.log',
|
||||
] as any);
|
||||
|
||||
const backups = await backupService.listBackups();
|
||||
|
||||
expect(backups).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs', async () => {
|
||||
(fs.readdir as any).mockRejectedValue(new Error('Read error'));
|
||||
|
||||
const backups = await backupService.listBackups();
|
||||
|
||||
expect(backups).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteBackup', () => {
|
||||
it('devrait supprimer un backup et ses métadonnées', async () => {
|
||||
await backupService.deleteBackup('backup.db.gz');
|
||||
|
||||
expect(fs.unlink).toHaveBeenCalledWith(
|
||||
path.join('/backups', 'backup.db.gz')
|
||||
);
|
||||
expect(fs.unlink).toHaveBeenCalledWith(
|
||||
path.join('/backups', 'backup.db.gz.meta.json')
|
||||
);
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs lors de la suppression', async () => {
|
||||
vi.mocked(fs.unlink).mockRejectedValue(new Error('Delete failed'));
|
||||
|
||||
await expect(
|
||||
backupService.deleteBackup('backup.db.gz')
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('restoreBackup', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(prisma.$queryRaw).mockResolvedValue([
|
||||
{ integrity_check: 'ok' },
|
||||
]);
|
||||
vi.spyOn(backupService, 'createBackup').mockResolvedValue({
|
||||
id: 'pre-restore',
|
||||
filename: 'pre-restore.db.gz',
|
||||
size: 1024,
|
||||
createdAt: new Date(),
|
||||
type: 'manual',
|
||||
status: 'success',
|
||||
} as any);
|
||||
});
|
||||
|
||||
it('devrait restaurer un backup non compressé', async () => {
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.copyFile).mockResolvedValue(undefined);
|
||||
|
||||
await backupService.restoreBackup('backup.db');
|
||||
|
||||
expect(fs.copyFile).toHaveBeenCalled();
|
||||
expect(prisma.$connect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait décompresser et restaurer un backup compressé', async () => {
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(BackupUtils.decompressFileTemp).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.copyFile).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.unlink).mockResolvedValue(undefined);
|
||||
|
||||
await backupService.restoreBackup('backup.db.gz');
|
||||
|
||||
expect(BackupUtils.decompressFileTemp).toHaveBeenCalled();
|
||||
expect(fs.copyFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs lors de la restauration', async () => {
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error('File not found'));
|
||||
|
||||
await expect(backupService.restoreBackup('backup.db')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyDatabaseHealth', () => {
|
||||
it("devrait vérifier l'intégrité de la base de données", async () => {
|
||||
vi.mocked(prisma.$queryRaw).mockResolvedValue([
|
||||
{ integrity_check: 'ok' },
|
||||
]);
|
||||
|
||||
await backupService.verifyDatabaseHealth();
|
||||
|
||||
expect(prisma.$queryRaw).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("devrait échouer si l'intégrité est compromise", async () => {
|
||||
vi.mocked(prisma.$queryRaw).mockResolvedValue([
|
||||
{ integrity_check: 'error in database' },
|
||||
]);
|
||||
|
||||
await expect(backupService.verifyDatabaseHealth()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasChangedSinceLastBackup', () => {
|
||||
it('devrait retourner true si aucun backup précédent', async () => {
|
||||
vi.mocked(fs.readdir).mockResolvedValue([]);
|
||||
|
||||
const hasChanged = await backupService.hasChangedSinceLastBackup();
|
||||
|
||||
expect(hasChanged).toBe(true);
|
||||
});
|
||||
|
||||
it('devrait retourner true si le hash a changé', async () => {
|
||||
vi.mocked(fs.readdir).mockResolvedValue([
|
||||
'towercontrol_manual_2024-01-01.db.gz',
|
||||
] as any);
|
||||
vi.mocked(BackupUtils.calculateFileHash)
|
||||
.mockResolvedValueOnce('newhash')
|
||||
.mockResolvedValueOnce('oldhash');
|
||||
vi.mocked(fs.readFile).mockResolvedValue(
|
||||
JSON.stringify({ databaseHash: 'oldhash' })
|
||||
);
|
||||
|
||||
const hasChanged = await backupService.hasChangedSinceLastBackup();
|
||||
|
||||
expect(hasChanged).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBackupLogs', () => {
|
||||
it('devrait lire les logs de backup', async () => {
|
||||
vi.mocked(fs.readFile).mockResolvedValue(
|
||||
'2024-01-01: Backup created\n2024-01-02: Backup created'
|
||||
);
|
||||
|
||||
const logs = await backupService.getBackupLogs();
|
||||
|
||||
expect(logs).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("devrait retourner une liste vide si le fichier n'existe pas", async () => {
|
||||
(fs.readFile as any).mockRejectedValue(new Error('File not found'));
|
||||
|
||||
const logs = await backupService.getBackupLogs();
|
||||
|
||||
expect(logs).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('devrait limiter le nombre de lignes', async () => {
|
||||
const manyLines = Array.from({ length: 200 }, (_, i) => `Line ${i}`).join(
|
||||
'\n'
|
||||
);
|
||||
vi.mocked(fs.readFile).mockResolvedValue(manyLines);
|
||||
|
||||
const logs = await backupService.getBackupLogs(50);
|
||||
|
||||
expect(logs.length).toBeLessThanOrEqual(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBackupStats', () => {
|
||||
it('devrait calculer les statistiques par jour', async () => {
|
||||
const now = new Date('2024-01-15');
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
vi.mocked(fs.readdir).mockResolvedValue([
|
||||
'towercontrol_manual_2024-01-15.db.gz',
|
||||
'towercontrol_automatic_2024-01-15.db.gz',
|
||||
'towercontrol_manual_2024-01-14.db.gz',
|
||||
] as any);
|
||||
vi.mocked(BackupUtils.parseBackupFilename)
|
||||
.mockReturnValueOnce({
|
||||
type: 'manual',
|
||||
date: new Date('2024-01-15'),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
type: 'automatic',
|
||||
date: new Date('2024-01-15'),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
type: 'manual',
|
||||
date: new Date('2024-01-14'),
|
||||
});
|
||||
|
||||
const stats = await backupService.getBackupStats(7);
|
||||
|
||||
expect(stats.length).toBeGreaterThan(0);
|
||||
const todayStats = stats.find((s) => s.date === '2024-01-15');
|
||||
expect(todayStats).toBeDefined();
|
||||
expect(todayStats?.manual).toBe(1);
|
||||
expect(todayStats?.automatic).toBe(1);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfig', () => {
|
||||
it('devrait retourner la configuration', async () => {
|
||||
const config = await backupService.getConfig();
|
||||
|
||||
expect(config).toHaveProperty('enabled');
|
||||
expect(config).toHaveProperty('interval');
|
||||
expect(config).toHaveProperty('maxBackups');
|
||||
expect(config).toHaveProperty('backupPath');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateConfig', () => {
|
||||
it('devrait mettre à jour la configuration', async () => {
|
||||
vi.mocked(prisma.userPreferences.upsert).mockResolvedValue({
|
||||
userId: 'default',
|
||||
} as any);
|
||||
|
||||
await backupService.updateConfig({ maxBackups: 10 });
|
||||
|
||||
expect(prisma.userPreferences.upsert).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Tests unitaires pour JiraAdvancedFiltersService
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { JiraAdvancedFiltersService } from '../advanced-filters';
|
||||
import { JiraTask, JiraAnalytics } from '@/lib/types';
|
||||
|
||||
describe('JiraAdvancedFiltersService', () => {
|
||||
const mockIssues: JiraTask[] = [
|
||||
{
|
||||
id: '1',
|
||||
key: 'TEST-1',
|
||||
summary: 'Task 1',
|
||||
status: { name: 'In Progress', category: 'inprogress' },
|
||||
issuetype: { name: 'Bug' },
|
||||
priority: { name: 'High' },
|
||||
assignee: { displayName: 'John Doe', emailAddress: 'john@example.com' },
|
||||
components: [{ name: 'Frontend' }],
|
||||
fixVersions: [{ name: 'v1.0' }],
|
||||
labels: ['urgent', 'bug'],
|
||||
project: { key: 'TEST', name: 'Test Project' },
|
||||
created: '2024-01-01',
|
||||
updated: '2024-01-02',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
key: 'TEST-2',
|
||||
summary: 'Task 2',
|
||||
status: { name: 'Done', category: 'done' },
|
||||
issuetype: { name: 'Story' },
|
||||
priority: { name: 'Medium' },
|
||||
assignee: { displayName: 'Jane Smith', emailAddress: 'jane@example.com' },
|
||||
components: [{ name: 'Backend' }],
|
||||
fixVersions: [{ name: 'v2.0' }],
|
||||
labels: ['feature'],
|
||||
project: { key: 'TEST', name: 'Test Project' },
|
||||
created: '2024-01-03',
|
||||
updated: '2024-01-04',
|
||||
},
|
||||
];
|
||||
|
||||
const mockAnalytics: JiraAnalytics = {
|
||||
project: {
|
||||
key: 'TEST',
|
||||
name: 'Test Project',
|
||||
totalIssues: 2,
|
||||
},
|
||||
teamMetrics: {
|
||||
totalAssignees: 2,
|
||||
activeAssignees: 2,
|
||||
issuesDistribution: [],
|
||||
},
|
||||
velocityMetrics: {
|
||||
currentSprintPoints: 0,
|
||||
averageVelocity: 0,
|
||||
sprintHistory: [],
|
||||
},
|
||||
cycleTimeMetrics: {
|
||||
averageCycleTime: 0,
|
||||
cycleTimeByType: [],
|
||||
},
|
||||
workInProgress: {
|
||||
byStatus: [],
|
||||
byAssignee: [],
|
||||
},
|
||||
};
|
||||
|
||||
describe('extractAvailableFilters', () => {
|
||||
it('devrait extraire tous les filtres disponibles', () => {
|
||||
const filters =
|
||||
JiraAdvancedFiltersService.extractAvailableFilters(mockIssues);
|
||||
|
||||
expect(filters.components).toHaveLength(2);
|
||||
expect(filters.fixVersions).toHaveLength(2);
|
||||
expect(filters.issueTypes).toHaveLength(2);
|
||||
expect(filters.statuses).toHaveLength(2);
|
||||
expect(filters.assignees).toHaveLength(2);
|
||||
expect(filters.labels).toHaveLength(3);
|
||||
expect(filters.priorities).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('devrait compter les occurrences correctement', () => {
|
||||
const filters =
|
||||
JiraAdvancedFiltersService.extractAvailableFilters(mockIssues);
|
||||
|
||||
const frontendComponent = filters.components.find(
|
||||
(c) => c.value === 'Frontend'
|
||||
);
|
||||
expect(frontendComponent?.count).toBe(1);
|
||||
|
||||
const urgentLabel = filters.labels.find((l) => l.value === 'urgent');
|
||||
expect(urgentLabel?.count).toBe(1);
|
||||
});
|
||||
|
||||
it('devrait gérer les issues sans assignee', () => {
|
||||
const issuesWithoutAssignee: JiraTask[] = [
|
||||
{
|
||||
...mockIssues[0],
|
||||
assignee: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const filters = JiraAdvancedFiltersService.extractAvailableFilters(
|
||||
issuesWithoutAssignee
|
||||
);
|
||||
|
||||
const unassigned = filters.assignees.find(
|
||||
(a) => a.value === 'Non assigné'
|
||||
);
|
||||
expect(unassigned).toBeDefined();
|
||||
});
|
||||
|
||||
it('devrait retourner des filtres vides pour une liste vide', () => {
|
||||
const filters = JiraAdvancedFiltersService.extractAvailableFilters([]);
|
||||
|
||||
expect(filters.components).toHaveLength(0);
|
||||
expect(filters.fixVersions).toHaveLength(0);
|
||||
expect(filters.issueTypes).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyFiltersToAnalytics', () => {
|
||||
it('devrait filtrer par composant', () => {
|
||||
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
|
||||
mockAnalytics,
|
||||
{ components: ['Frontend'] },
|
||||
mockIssues
|
||||
);
|
||||
|
||||
expect(filtered.project.totalIssues).toBe(1);
|
||||
});
|
||||
|
||||
it('devrait filtrer par type de ticket', () => {
|
||||
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
|
||||
mockAnalytics,
|
||||
{ issueTypes: ['Bug'] },
|
||||
mockIssues
|
||||
);
|
||||
|
||||
expect(filtered.project.totalIssues).toBe(1);
|
||||
});
|
||||
|
||||
it('devrait filtrer par statut', () => {
|
||||
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
|
||||
mockAnalytics,
|
||||
{ statuses: ['In Progress'] },
|
||||
mockIssues
|
||||
);
|
||||
|
||||
expect(filtered.project.totalIssues).toBe(1);
|
||||
});
|
||||
|
||||
it('devrait filtrer par assignee', () => {
|
||||
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
|
||||
mockAnalytics,
|
||||
{ assignees: ['John Doe'] },
|
||||
mockIssues
|
||||
);
|
||||
|
||||
expect(filtered.project.totalIssues).toBe(1);
|
||||
});
|
||||
|
||||
it('devrait filtrer par label', () => {
|
||||
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
|
||||
mockAnalytics,
|
||||
{ labels: ['urgent'] },
|
||||
mockIssues
|
||||
);
|
||||
|
||||
expect(filtered.project.totalIssues).toBe(1);
|
||||
});
|
||||
|
||||
it('devrait appliquer plusieurs filtres simultanément', () => {
|
||||
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
|
||||
mockAnalytics,
|
||||
{
|
||||
components: ['Frontend'],
|
||||
issueTypes: ['Bug'],
|
||||
},
|
||||
mockIssues
|
||||
);
|
||||
|
||||
expect(filtered.project.totalIssues).toBe(1);
|
||||
});
|
||||
|
||||
it('devrait retourner analytics vide si aucun match', () => {
|
||||
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
|
||||
mockAnalytics,
|
||||
{ components: ['NonExistent'] },
|
||||
mockIssues
|
||||
);
|
||||
|
||||
expect(filtered.project.totalIssues).toBe(0);
|
||||
});
|
||||
|
||||
it('devrait retourner analytics original si pas de filtres', () => {
|
||||
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
|
||||
mockAnalytics,
|
||||
{},
|
||||
mockIssues
|
||||
);
|
||||
|
||||
expect(filtered.project.totalIssues).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
221
src/services/integrations/jira/__tests__/analytics-cache.test.ts
Normal file
221
src/services/integrations/jira/__tests__/analytics-cache.test.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Tests unitaires pour JiraAnalyticsCacheService
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { jiraAnalyticsCache } from '../analytics-cache';
|
||||
import { JiraAnalytics } from '@/lib/types';
|
||||
|
||||
describe('JiraAnalyticsCacheService', () => {
|
||||
const mockConfig = {
|
||||
baseUrl: 'https://test.atlassian.net',
|
||||
email: 'test@example.com',
|
||||
apiToken: 'token123',
|
||||
projectKey: 'TEST',
|
||||
};
|
||||
|
||||
const mockAnalytics: JiraAnalytics = {
|
||||
project: {
|
||||
key: 'TEST',
|
||||
name: 'Test Project',
|
||||
totalIssues: 10,
|
||||
},
|
||||
teamMetrics: {
|
||||
totalAssignees: 5,
|
||||
activeAssignees: 5,
|
||||
issuesDistribution: [],
|
||||
},
|
||||
velocityMetrics: {
|
||||
currentSprintPoints: 0,
|
||||
averageVelocity: 0,
|
||||
sprintHistory: [],
|
||||
},
|
||||
cycleTimeMetrics: {
|
||||
averageCycleTime: 0,
|
||||
cycleTimeByType: [],
|
||||
},
|
||||
workInProgress: {
|
||||
byStatus: [],
|
||||
byAssignee: [],
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jiraAnalyticsCache.invalidateAll();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jiraAnalyticsCache.invalidateAll();
|
||||
jiraAnalyticsCache.stopCleanupInterval();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('set et get', () => {
|
||||
it('devrait stocker et récupérer les analytics', () => {
|
||||
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
|
||||
|
||||
const result = jiraAnalyticsCache.get(mockConfig);
|
||||
|
||||
expect(result).toEqual(mockAnalytics);
|
||||
});
|
||||
|
||||
it('devrait retourner null si pas de cache', () => {
|
||||
const result = jiraAnalyticsCache.get(mockConfig);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('devrait utiliser un TTL personnalisé', () => {
|
||||
const customTTL = 60 * 60 * 1000; // 1 heure
|
||||
jiraAnalyticsCache.set(mockConfig, mockAnalytics, customTTL);
|
||||
|
||||
const result = jiraAnalyticsCache.get(mockConfig);
|
||||
expect(result).toEqual(mockAnalytics);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalidate', () => {
|
||||
it('devrait invalider le cache pour un projet spécifique', () => {
|
||||
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
|
||||
|
||||
jiraAnalyticsCache.invalidate(mockConfig);
|
||||
|
||||
const result = jiraAnalyticsCache.get(mockConfig);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('devrait invalider tout le cache', () => {
|
||||
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
|
||||
|
||||
jiraAnalyticsCache.invalidateAll();
|
||||
|
||||
const result = jiraAnalyticsCache.get(mockConfig);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('expiration', () => {
|
||||
it('devrait expirer après le TTL', () => {
|
||||
const shortTTL = 1000; // 1 seconde
|
||||
jiraAnalyticsCache.set(mockConfig, mockAnalytics, shortTTL);
|
||||
|
||||
// Avancer le temps de 2 secondes
|
||||
vi.advanceTimersByTime(2000);
|
||||
|
||||
const result = jiraAnalyticsCache.get(mockConfig);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('ne devrait pas expirer avant le TTL', () => {
|
||||
const shortTTL = 5000; // 5 secondes
|
||||
jiraAnalyticsCache.set(mockConfig, mockAnalytics, shortTTL);
|
||||
|
||||
// Avancer le temps de 2 secondes seulement
|
||||
vi.advanceTimersByTime(2000);
|
||||
|
||||
const result = jiraAnalyticsCache.get(mockConfig);
|
||||
expect(result).toEqual(mockAnalytics);
|
||||
});
|
||||
});
|
||||
|
||||
describe('config hash', () => {
|
||||
it('devrait invalider si la config change', () => {
|
||||
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
|
||||
|
||||
const newConfig = {
|
||||
...mockConfig,
|
||||
apiToken: 'newtoken',
|
||||
};
|
||||
|
||||
const result = jiraAnalyticsCache.get(newConfig);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('devrait garder le cache si la config est identique', () => {
|
||||
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
|
||||
|
||||
const sameConfig = { ...mockConfig };
|
||||
const result = jiraAnalyticsCache.get(sameConfig);
|
||||
|
||||
expect(result).toEqual(mockAnalytics);
|
||||
});
|
||||
});
|
||||
|
||||
describe('has', () => {
|
||||
it('devrait retourner true si le cache existe', () => {
|
||||
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
|
||||
|
||||
const exists = jiraAnalyticsCache.has(mockConfig);
|
||||
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
|
||||
it("devrait retourner false si le cache n'existe pas", () => {
|
||||
const exists = jiraAnalyticsCache.has(mockConfig);
|
||||
|
||||
expect(exists).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats', () => {
|
||||
it('devrait retourner les statistiques du cache', () => {
|
||||
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
|
||||
|
||||
const stats = jiraAnalyticsCache.getStats();
|
||||
|
||||
expect(stats.totalEntries).toBe(1);
|
||||
expect(stats.projects).toHaveLength(1);
|
||||
expect(stats.projects[0].projectKey).toBe('TEST');
|
||||
});
|
||||
|
||||
it('devrait retourner des stats vides si pas de cache', () => {
|
||||
const stats = jiraAnalyticsCache.getStats();
|
||||
|
||||
expect(stats.totalEntries).toBe(0);
|
||||
expect(stats.projects).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('forceCleanup', () => {
|
||||
it('devrait nettoyer les entrées expirées', () => {
|
||||
const shortTTL = 1000; // 1 seconde
|
||||
jiraAnalyticsCache.set(mockConfig, mockAnalytics, shortTTL);
|
||||
|
||||
// Avancer le temps pour expirer
|
||||
vi.advanceTimersByTime(2000);
|
||||
|
||||
const cleanedCount = jiraAnalyticsCache.forceCleanup();
|
||||
|
||||
expect(cleanedCount).toBe(1);
|
||||
expect(jiraAnalyticsCache.get(mockConfig)).toBeNull();
|
||||
});
|
||||
|
||||
it('devrait retourner 0 si aucune entrée expirée', () => {
|
||||
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
|
||||
|
||||
const cleanedCount = jiraAnalyticsCache.forceCleanup();
|
||||
|
||||
expect(cleanedCount).toBe(0);
|
||||
expect(jiraAnalyticsCache.get(mockConfig)).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple projects', () => {
|
||||
it('devrait gérer plusieurs projets indépendamment', () => {
|
||||
const config1 = { ...mockConfig, projectKey: 'PROJ1' };
|
||||
const config2 = { ...mockConfig, projectKey: 'PROJ2' };
|
||||
|
||||
jiraAnalyticsCache.set(config1, mockAnalytics);
|
||||
jiraAnalyticsCache.set(config2, mockAnalytics);
|
||||
|
||||
expect(jiraAnalyticsCache.get(config1)).toEqual(mockAnalytics);
|
||||
expect(jiraAnalyticsCache.get(config2)).toEqual(mockAnalytics);
|
||||
|
||||
jiraAnalyticsCache.invalidate(config1);
|
||||
|
||||
expect(jiraAnalyticsCache.get(config1)).toBeNull();
|
||||
expect(jiraAnalyticsCache.get(config2)).toEqual(mockAnalytics);
|
||||
});
|
||||
});
|
||||
});
|
||||
131
src/services/integrations/jira/__tests__/analytics.test.ts
Normal file
131
src/services/integrations/jira/__tests__/analytics.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Tests unitaires pour JiraAnalyticsService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { JiraAnalyticsService } from '../analytics';
|
||||
import { JiraService } from '../jira';
|
||||
import { jiraAnalyticsCache } from '../analytics-cache';
|
||||
|
||||
// Mock de JiraService
|
||||
vi.mock('../jira', () => ({
|
||||
JiraService: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock de analytics-cache
|
||||
vi.mock('../analytics-cache', () => ({
|
||||
jiraAnalyticsCache: {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('JiraAnalyticsService', () => {
|
||||
let service: JiraAnalyticsService;
|
||||
let mockJiraService: any;
|
||||
|
||||
const mockConfig = {
|
||||
enabled: true,
|
||||
baseUrl: 'https://test.atlassian.net',
|
||||
email: 'test@example.com',
|
||||
apiToken: 'token123',
|
||||
projectKey: 'TEST',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockJiraService = {
|
||||
searchIssues: vi.fn(),
|
||||
validateProject: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mocked(JiraService).mockImplementation(() => mockJiraService as any);
|
||||
vi.mocked(jiraAnalyticsCache.get).mockReturnValue(null);
|
||||
|
||||
service = new JiraAnalyticsService(mockConfig);
|
||||
});
|
||||
|
||||
describe('getAllProjectIssues', () => {
|
||||
it('devrait récupérer toutes les issues du projet', async () => {
|
||||
const mockIssues = [
|
||||
{ key: 'TEST-1', title: 'Issue 1' },
|
||||
{ key: 'TEST-2', title: 'Issue 2' },
|
||||
];
|
||||
|
||||
vi.mocked(mockJiraService.searchIssues).mockResolvedValue(mockIssues);
|
||||
|
||||
const issues = await service.getAllProjectIssues();
|
||||
|
||||
expect(issues).toEqual(mockIssues);
|
||||
expect(mockJiraService.searchIssues).toHaveBeenCalledWith(
|
||||
'project = "TEST" ORDER BY created DESC'
|
||||
);
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs', async () => {
|
||||
vi.mocked(mockJiraService.searchIssues).mockRejectedValue(
|
||||
new Error('API Error')
|
||||
);
|
||||
|
||||
await expect(service.getAllProjectIssues()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProjectAnalytics', () => {
|
||||
it('devrait retourner les analytics depuis le cache si disponible', async () => {
|
||||
const cachedAnalytics = {
|
||||
project: { key: 'TEST', name: 'Test', totalIssues: 0 },
|
||||
teamMetrics: {
|
||||
totalAssignees: 0,
|
||||
activeAssignees: 0,
|
||||
issuesDistribution: [],
|
||||
},
|
||||
velocityMetrics: {
|
||||
currentSprintPoints: 0,
|
||||
sprintHistory: [],
|
||||
averageVelocity: 0,
|
||||
},
|
||||
cycleTimeMetrics: { averageCycleTime: 0, cycleTimeByType: [] },
|
||||
workInProgress: { byStatus: [], byAssignee: [] },
|
||||
};
|
||||
|
||||
vi.mocked(jiraAnalyticsCache.get).mockReturnValue(cachedAnalytics as any);
|
||||
|
||||
const analytics = await service.getProjectAnalytics();
|
||||
|
||||
expect(analytics).toEqual(cachedAnalytics);
|
||||
expect(mockJiraService.searchIssues).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait forcer le refresh si demandé', async () => {
|
||||
const cachedAnalytics = {
|
||||
project: { key: 'TEST', name: 'Test', totalIssues: 0 },
|
||||
teamMetrics: {
|
||||
totalAssignees: 0,
|
||||
activeAssignees: 0,
|
||||
issuesDistribution: [],
|
||||
},
|
||||
velocityMetrics: {
|
||||
currentSprintPoints: 0,
|
||||
sprintHistory: [],
|
||||
averageVelocity: 0,
|
||||
},
|
||||
cycleTimeMetrics: { averageCycleTime: 0, cycleTimeByType: [] },
|
||||
workInProgress: { byStatus: [], byAssignee: [] },
|
||||
};
|
||||
|
||||
vi.mocked(jiraAnalyticsCache.get).mockReturnValue(cachedAnalytics as any);
|
||||
vi.mocked(mockJiraService.validateProject).mockResolvedValue({
|
||||
exists: true,
|
||||
name: 'Test Project',
|
||||
});
|
||||
vi.mocked(mockJiraService.searchIssues).mockResolvedValue([]);
|
||||
|
||||
await service.getProjectAnalytics(true);
|
||||
|
||||
expect(mockJiraService.searchIssues).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* Tests unitaires pour JiraAnomalyDetectionService
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { JiraAnomalyDetectionService } from '../anomaly-detection';
|
||||
import { JiraAnalytics } from '@/lib/types';
|
||||
import { getToday } from '@/lib/date-utils';
|
||||
|
||||
// Mock de date-utils
|
||||
vi.mock('@/lib/date-utils', () => ({
|
||||
getToday: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('JiraAnomalyDetectionService', () => {
|
||||
let service: JiraAnomalyDetectionService;
|
||||
|
||||
const mockAnalytics: JiraAnalytics = {
|
||||
project: {
|
||||
key: 'TEST',
|
||||
name: 'Test Project',
|
||||
totalIssues: 10,
|
||||
},
|
||||
teamMetrics: {
|
||||
totalAssignees: 2,
|
||||
activeAssignees: 2,
|
||||
issuesDistribution: [],
|
||||
},
|
||||
velocityMetrics: {
|
||||
currentSprintPoints: 0,
|
||||
sprintHistory: [
|
||||
{
|
||||
sprintName: 'Sprint 1',
|
||||
startDate: '2024-01-01',
|
||||
endDate: '2024-01-14',
|
||||
completedPoints: 20,
|
||||
plannedPoints: 20,
|
||||
completionRate: 100,
|
||||
velocity: 20,
|
||||
},
|
||||
{
|
||||
sprintName: 'Sprint 2',
|
||||
startDate: '2024-01-15',
|
||||
endDate: '2024-01-28',
|
||||
completedPoints: 5,
|
||||
plannedPoints: 20,
|
||||
completionRate: 25,
|
||||
velocity: 5,
|
||||
}, // Variance importante
|
||||
{
|
||||
sprintName: 'Sprint 3',
|
||||
startDate: '2024-01-29',
|
||||
endDate: '2024-02-11',
|
||||
completedPoints: 25,
|
||||
plannedPoints: 20,
|
||||
completionRate: 125,
|
||||
velocity: 25,
|
||||
},
|
||||
],
|
||||
averageVelocity: 16.67,
|
||||
},
|
||||
cycleTimeMetrics: {
|
||||
averageCycleTime: 12.5,
|
||||
cycleTimeByType: [
|
||||
{ issueType: 'Bug', averageDays: 5, medianDays: 4, samples: 10 },
|
||||
{ issueType: 'Story', averageDays: 20, medianDays: 18, samples: 5 }, // Cycle time élevé
|
||||
],
|
||||
},
|
||||
workInProgress: {
|
||||
byStatus: [],
|
||||
byAssignee: [
|
||||
{
|
||||
assignee: 'john@example.com',
|
||||
displayName: 'John',
|
||||
todoCount: 3,
|
||||
inProgressCount: 5,
|
||||
reviewCount: 0,
|
||||
totalActive: 8,
|
||||
},
|
||||
{
|
||||
assignee: 'jane@example.com',
|
||||
displayName: 'Jane',
|
||||
todoCount: 1,
|
||||
inProgressCount: 1,
|
||||
reviewCount: 0,
|
||||
totalActive: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getToday).mockReturnValue(new Date('2024-01-15'));
|
||||
service = new JiraAnomalyDetectionService();
|
||||
});
|
||||
|
||||
describe('detectAnomalies', () => {
|
||||
it('devrait détecter des anomalies de vélocité', async () => {
|
||||
const anomalies = await service.detectAnomalies(mockAnalytics);
|
||||
|
||||
const velocityAnomalies = anomalies.filter((a) => a.type === 'velocity');
|
||||
expect(velocityAnomalies.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('devrait détecter des anomalies de cycle time', async () => {
|
||||
const anomalies = await service.detectAnomalies(mockAnalytics);
|
||||
|
||||
const cycleTimeAnomalies = anomalies.filter(
|
||||
(a) => a.type === 'cycle_time'
|
||||
);
|
||||
expect(cycleTimeAnomalies.length).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('devrait détecter des déséquilibres de charge', async () => {
|
||||
const anomalies = await service.detectAnomalies(mockAnalytics);
|
||||
|
||||
const workloadAnomalies = anomalies.filter((a) => a.type === 'workload');
|
||||
expect(workloadAnomalies.length).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('devrait trier les anomalies par sévérité', async () => {
|
||||
const anomalies = await service.detectAnomalies(mockAnalytics);
|
||||
|
||||
const severityWeights = {
|
||||
critical: 4,
|
||||
high: 3,
|
||||
medium: 2,
|
||||
low: 1,
|
||||
};
|
||||
|
||||
for (let i = 0; i < anomalies.length - 1; i++) {
|
||||
const current = severityWeights[anomalies[i].severity];
|
||||
const next = severityWeights[anomalies[i + 1].severity];
|
||||
expect(current).toBeGreaterThanOrEqual(next);
|
||||
}
|
||||
});
|
||||
|
||||
it('devrait retourner une liste vide si aucune anomalie', async () => {
|
||||
const cleanAnalytics: JiraAnalytics = {
|
||||
...mockAnalytics,
|
||||
velocityMetrics: {
|
||||
currentSprintPoints: 0,
|
||||
sprintHistory: [
|
||||
{
|
||||
sprintName: 'Sprint 1',
|
||||
startDate: '2024-01-01',
|
||||
endDate: '2024-01-14',
|
||||
completedPoints: 20,
|
||||
plannedPoints: 20,
|
||||
completionRate: 100,
|
||||
velocity: 20,
|
||||
},
|
||||
{
|
||||
sprintName: 'Sprint 2',
|
||||
startDate: '2024-01-15',
|
||||
endDate: '2024-01-28',
|
||||
completedPoints: 21,
|
||||
plannedPoints: 20,
|
||||
completionRate: 105,
|
||||
velocity: 21,
|
||||
},
|
||||
{
|
||||
sprintName: 'Sprint 3',
|
||||
startDate: '2024-01-29',
|
||||
endDate: '2024-02-11',
|
||||
completedPoints: 19,
|
||||
plannedPoints: 20,
|
||||
completionRate: 95,
|
||||
velocity: 19,
|
||||
},
|
||||
],
|
||||
averageVelocity: 20,
|
||||
},
|
||||
cycleTimeMetrics: {
|
||||
averageCycleTime: 5,
|
||||
cycleTimeByType: [
|
||||
{ issueType: 'Story', averageDays: 5, medianDays: 4, samples: 10 },
|
||||
],
|
||||
},
|
||||
workInProgress: {
|
||||
byStatus: [],
|
||||
byAssignee: [
|
||||
{
|
||||
assignee: 'john@example.com',
|
||||
displayName: 'John',
|
||||
todoCount: 2,
|
||||
inProgressCount: 1,
|
||||
reviewCount: 0,
|
||||
totalActive: 3,
|
||||
},
|
||||
{
|
||||
assignee: 'jane@example.com',
|
||||
displayName: 'Jane',
|
||||
todoCount: 1,
|
||||
inProgressCount: 1,
|
||||
reviewCount: 0,
|
||||
totalActive: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const anomalies = await service.detectAnomalies(cleanAnalytics);
|
||||
// Peut avoir quelques anomalies même avec des données propres selon les seuils
|
||||
expect(Array.isArray(anomalies)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('config personnalisée', () => {
|
||||
it('devrait utiliser une config personnalisée', () => {
|
||||
const customConfig = {
|
||||
velocityVarianceThreshold: 50,
|
||||
cycleTimeThreshold: 3.0,
|
||||
};
|
||||
|
||||
const customService = new JiraAnomalyDetectionService(customConfig);
|
||||
expect(customService).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
144
src/services/integrations/jira/__tests__/jira.test.ts
Normal file
144
src/services/integrations/jira/__tests__/jira.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Tests unitaires pour JiraService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { JiraService } from '../jira';
|
||||
|
||||
// Mock de fetch global
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe('JiraService', () => {
|
||||
let service: JiraService;
|
||||
|
||||
const mockConfig = {
|
||||
enabled: true,
|
||||
baseUrl: 'https://test.atlassian.net',
|
||||
email: 'test@example.com',
|
||||
apiToken: 'token123',
|
||||
projectKey: 'TEST',
|
||||
ignoredProjects: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
service = new JiraService(mockConfig);
|
||||
});
|
||||
|
||||
describe('testConnection', () => {
|
||||
it('devrait retourner true si la connexion réussit', async () => {
|
||||
vi.mocked(fetch).mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: vi.fn().mockResolvedValue(''),
|
||||
} as any);
|
||||
|
||||
const result = await service.testConnection();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(fetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait retourner false si la connexion échoue', async () => {
|
||||
vi.mocked(fetch).mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
text: vi.fn().mockResolvedValue('Unauthorized'),
|
||||
} as any);
|
||||
|
||||
const result = await service.testConnection();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs de réseau', async () => {
|
||||
vi.mocked(fetch).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const result = await service.testConnection();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateConfig', () => {
|
||||
it('devrait valider une config complète', async () => {
|
||||
vi.mocked(fetch).mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: vi.fn().mockResolvedValue(''),
|
||||
} as any);
|
||||
|
||||
const result = await service.validateConfig();
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('devrait rejeter une config sans baseUrl', async () => {
|
||||
const invalidConfig = {
|
||||
...mockConfig,
|
||||
baseUrl: undefined,
|
||||
};
|
||||
const invalidService = new JiraService(invalidConfig as any);
|
||||
|
||||
const result = await invalidService.validateConfig();
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('URL de base');
|
||||
});
|
||||
|
||||
it('devrait rejeter une config sans email', async () => {
|
||||
const invalidConfig = {
|
||||
...mockConfig,
|
||||
email: undefined,
|
||||
};
|
||||
const invalidService = new JiraService(invalidConfig as any);
|
||||
|
||||
const result = await invalidService.validateConfig();
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('Email');
|
||||
});
|
||||
|
||||
it('devrait rejeter une config sans apiToken', async () => {
|
||||
const invalidConfig = {
|
||||
...mockConfig,
|
||||
apiToken: undefined,
|
||||
};
|
||||
const invalidService = new JiraService(invalidConfig as any);
|
||||
|
||||
const result = await invalidService.validateConfig();
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('Token API');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateProject', () => {
|
||||
it('devrait valider un projet existant', async () => {
|
||||
vi.mocked(fetch).mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: vi.fn().mockResolvedValue({ name: 'Test Project' }),
|
||||
} as any);
|
||||
|
||||
const result = await service.validateProject('TEST');
|
||||
|
||||
expect(result.exists).toBe(true);
|
||||
expect(result.name).toBe('Test Project');
|
||||
});
|
||||
|
||||
it('devrait détecter un projet inexistant', async () => {
|
||||
vi.mocked(fetch).mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
text: vi.fn().mockResolvedValue('Not found'),
|
||||
} as any);
|
||||
|
||||
const result = await service.validateProject('INVALID');
|
||||
|
||||
expect(result.exists).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
333
src/services/integrations/jira/__tests__/scheduler.test.ts
Normal file
333
src/services/integrations/jira/__tests__/scheduler.test.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* Tests unitaires pour JiraScheduler
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { JiraScheduler } from '../scheduler';
|
||||
import { userPreferencesService } from '@/services/core/user-preferences';
|
||||
import { JiraService } from '../jira';
|
||||
import { getToday, addMinutes } from '@/lib/date-utils';
|
||||
|
||||
// Mock de userPreferencesService
|
||||
vi.mock('@/services/core/user-preferences', () => ({
|
||||
userPreferencesService: {
|
||||
getJiraConfig: vi.fn(),
|
||||
getJiraSchedulerConfig: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock de JiraService
|
||||
vi.mock('../jira', () => ({
|
||||
JiraService: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock de date-utils
|
||||
vi.mock('@/lib/date-utils', () => ({
|
||||
getToday: vi.fn(),
|
||||
addMinutes: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('JiraScheduler', () => {
|
||||
let scheduler: JiraScheduler;
|
||||
let mockJiraService: any;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
scheduler = new JiraScheduler();
|
||||
vi.useFakeTimers();
|
||||
|
||||
// Mock JiraService
|
||||
mockJiraService = {
|
||||
testConnection: vi.fn(),
|
||||
syncTasks: vi.fn(),
|
||||
};
|
||||
vi.mocked(JiraService).mockImplementation(() => mockJiraService as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
scheduler.stop();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
it('devrait démarrer le scheduler si activé et configuré', async () => {
|
||||
vi.mocked(
|
||||
userPreferencesService.getJiraSchedulerConfig
|
||||
).mockResolvedValue({
|
||||
jiraAutoSync: true,
|
||||
jiraSyncInterval: 'hourly',
|
||||
} as any);
|
||||
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
baseUrl: 'https://test.atlassian.net',
|
||||
email: 'test@example.com',
|
||||
apiToken: 'token123',
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
|
||||
expect(scheduler.isActive()).toBe(true);
|
||||
});
|
||||
|
||||
it('ne devrait pas démarrer si désactivé', async () => {
|
||||
vi.mocked(
|
||||
userPreferencesService.getJiraSchedulerConfig
|
||||
).mockResolvedValue({
|
||||
jiraAutoSync: false,
|
||||
jiraSyncInterval: 'hourly',
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
|
||||
expect(scheduler.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('ne devrait pas démarrer si Jira non configuré', async () => {
|
||||
vi.mocked(
|
||||
userPreferencesService.getJiraSchedulerConfig
|
||||
).mockResolvedValue({
|
||||
jiraAutoSync: true,
|
||||
jiraSyncInterval: 'hourly',
|
||||
} as any);
|
||||
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
|
||||
enabled: false,
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
|
||||
expect(scheduler.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('ne devrait pas démarrer deux fois', async () => {
|
||||
vi.mocked(
|
||||
userPreferencesService.getJiraSchedulerConfig
|
||||
).mockResolvedValue({
|
||||
jiraAutoSync: true,
|
||||
jiraSyncInterval: 'hourly',
|
||||
} as any);
|
||||
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
baseUrl: 'https://test.atlassian.net',
|
||||
email: 'test@example.com',
|
||||
apiToken: 'token123',
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
const firstCall = scheduler.isActive();
|
||||
await scheduler.start('user1');
|
||||
const secondCall = scheduler.isActive();
|
||||
|
||||
expect(firstCall).toBe(true);
|
||||
expect(secondCall).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stop', () => {
|
||||
it('devrait arrêter le scheduler', async () => {
|
||||
vi.mocked(
|
||||
userPreferencesService.getJiraSchedulerConfig
|
||||
).mockResolvedValue({
|
||||
jiraAutoSync: true,
|
||||
jiraSyncInterval: 'hourly',
|
||||
} as any);
|
||||
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
baseUrl: 'https://test.atlassian.net',
|
||||
email: 'test@example.com',
|
||||
apiToken: 'token123',
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
expect(scheduler.isActive()).toBe(true);
|
||||
|
||||
scheduler.stop();
|
||||
expect(scheduler.isActive()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restart', () => {
|
||||
it('devrait redémarrer le scheduler', async () => {
|
||||
vi.mocked(
|
||||
userPreferencesService.getJiraSchedulerConfig
|
||||
).mockResolvedValue({
|
||||
jiraAutoSync: true,
|
||||
jiraSyncInterval: 'hourly',
|
||||
} as any);
|
||||
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
baseUrl: 'https://test.atlassian.net',
|
||||
email: 'test@example.com',
|
||||
apiToken: 'token123',
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
expect(scheduler.isActive()).toBe(true);
|
||||
|
||||
await scheduler.restart('user1');
|
||||
expect(scheduler.isActive()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isActive', () => {
|
||||
it('devrait retourner false si pas démarré', () => {
|
||||
expect(scheduler.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('devrait retourner true si démarré', async () => {
|
||||
vi.mocked(
|
||||
userPreferencesService.getJiraSchedulerConfig
|
||||
).mockResolvedValue({
|
||||
jiraAutoSync: true,
|
||||
jiraSyncInterval: 'hourly',
|
||||
} as any);
|
||||
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
baseUrl: 'https://test.atlassian.net',
|
||||
email: 'test@example.com',
|
||||
apiToken: 'token123',
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
expect(scheduler.isActive()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('performScheduledSync', () => {
|
||||
it('devrait exécuter une synchronisation automatique', async () => {
|
||||
vi.mocked(
|
||||
userPreferencesService.getJiraSchedulerConfig
|
||||
).mockResolvedValue({
|
||||
jiraAutoSync: true,
|
||||
jiraSyncInterval: 'hourly',
|
||||
} as any);
|
||||
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
baseUrl: 'https://test.atlassian.net',
|
||||
email: 'test@example.com',
|
||||
apiToken: 'token123',
|
||||
} as any);
|
||||
vi.mocked(mockJiraService.testConnection).mockResolvedValue(true);
|
||||
vi.mocked(mockJiraService.syncTasks).mockResolvedValue({
|
||||
success: true,
|
||||
stats: {
|
||||
created: 5,
|
||||
updated: 3,
|
||||
skipped: 1,
|
||||
},
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
|
||||
// Avancer le timer pour déclencher la synchronisation
|
||||
vi.advanceTimersByTime(60 * 60 * 1000); // 1 heure
|
||||
|
||||
// Attendre que la synchronisation soit exécutée
|
||||
await vi.waitFor(() => {
|
||||
expect(mockJiraService.syncTasks).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
scheduler.stop();
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs de connexion', async () => {
|
||||
vi.mocked(
|
||||
userPreferencesService.getJiraSchedulerConfig
|
||||
).mockResolvedValue({
|
||||
jiraAutoSync: true,
|
||||
jiraSyncInterval: 'hourly',
|
||||
} as any);
|
||||
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
baseUrl: 'https://test.atlassian.net',
|
||||
email: 'test@example.com',
|
||||
apiToken: 'token123',
|
||||
} as any);
|
||||
vi.mocked(mockJiraService.testConnection).mockResolvedValue(false);
|
||||
|
||||
await scheduler.start('user1');
|
||||
|
||||
vi.advanceTimersByTime(60 * 60 * 1000);
|
||||
await vi.waitFor(() => {
|
||||
expect(mockJiraService.testConnection).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
scheduler.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextSyncTime', () => {
|
||||
it('devrait retourner null si pas démarré', async () => {
|
||||
const nextTime = await scheduler.getNextSyncTime();
|
||||
|
||||
expect(nextTime).toBeNull();
|
||||
});
|
||||
|
||||
it('devrait calculer le prochain moment de synchronisation', async () => {
|
||||
const mockDate = new Date('2024-01-15T12:00:00Z');
|
||||
vi.mocked(getToday).mockReturnValue(mockDate);
|
||||
vi.mocked(addMinutes).mockReturnValue(new Date('2024-01-15T13:00:00Z'));
|
||||
|
||||
vi.mocked(
|
||||
userPreferencesService.getJiraSchedulerConfig
|
||||
).mockResolvedValue({
|
||||
jiraAutoSync: true,
|
||||
jiraSyncInterval: 'hourly',
|
||||
} as any);
|
||||
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
baseUrl: 'https://test.atlassian.net',
|
||||
email: 'test@example.com',
|
||||
apiToken: 'token123',
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
const nextTime = await scheduler.getNextSyncTime();
|
||||
|
||||
expect(nextTime).not.toBeNull();
|
||||
expect(addMinutes).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatus', () => {
|
||||
it('devrait retourner le statut du scheduler', async () => {
|
||||
vi.mocked(
|
||||
userPreferencesService.getJiraSchedulerConfig
|
||||
).mockResolvedValue({
|
||||
jiraAutoSync: true,
|
||||
jiraSyncInterval: 'daily',
|
||||
} as any);
|
||||
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
baseUrl: 'https://test.atlassian.net',
|
||||
email: 'test@example.com',
|
||||
apiToken: 'token123',
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
const status = await scheduler.getStatus('user1');
|
||||
|
||||
expect(status.isRunning).toBe(true);
|
||||
expect(status.isEnabled).toBe(true);
|
||||
expect(status.interval).toBe('daily');
|
||||
expect(status.jiraConfigured).toBe(true);
|
||||
});
|
||||
|
||||
it('devrait retourner le statut si pas démarré', async () => {
|
||||
vi.mocked(
|
||||
userPreferencesService.getJiraSchedulerConfig
|
||||
).mockResolvedValue({
|
||||
jiraAutoSync: false,
|
||||
jiraSyncInterval: 'hourly',
|
||||
} as any);
|
||||
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
|
||||
enabled: false,
|
||||
} as any);
|
||||
|
||||
const status = await scheduler.getStatus('user1');
|
||||
|
||||
expect(status.isRunning).toBe(false);
|
||||
expect(status.isEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -426,6 +426,7 @@ describe('Synchronisation Jira - Logique de préservation des champs', () => {
|
||||
|
||||
expect(realChanges.length).toBeGreaterThan(0);
|
||||
expect(realChanges.some((c) => c.includes('Projet'))).toBe(true);
|
||||
expect(preservedFields).toHaveLength(0); // Pas de préservation pour un changement de projet
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
335
src/services/integrations/tfs/__tests__/scheduler.test.ts
Normal file
335
src/services/integrations/tfs/__tests__/scheduler.test.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* Tests unitaires pour TfsScheduler
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { TfsScheduler } from '../scheduler';
|
||||
import { userPreferencesService } from '@/services/core/user-preferences';
|
||||
import { TfsService } from '../tfs';
|
||||
import { prisma } from '@/services/core/database';
|
||||
import { getToday, addMinutes } from '@/lib/date-utils';
|
||||
|
||||
// Mock de userPreferencesService
|
||||
vi.mock('@/services/core/user-preferences', () => ({
|
||||
userPreferencesService: {
|
||||
getTfsConfig: vi.fn(),
|
||||
getTfsSchedulerConfig: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock de TfsService
|
||||
vi.mock('../tfs', () => ({
|
||||
TfsService: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock de prisma
|
||||
vi.mock('@/services/core/database', () => ({
|
||||
prisma: {
|
||||
user: {
|
||||
findFirst: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock de date-utils
|
||||
vi.mock('@/lib/date-utils', () => ({
|
||||
getToday: vi.fn(),
|
||||
addMinutes: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('TfsScheduler', () => {
|
||||
let scheduler: TfsScheduler;
|
||||
let mockTfsService: any;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
scheduler = new TfsScheduler();
|
||||
vi.useFakeTimers();
|
||||
|
||||
// Mock TfsService
|
||||
mockTfsService = {
|
||||
testConnection: vi.fn(),
|
||||
syncTasks: vi.fn(),
|
||||
};
|
||||
vi.mocked(TfsService).mockImplementation(() => mockTfsService as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
scheduler.stop();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
it('devrait démarrer le scheduler si activé et configuré', async () => {
|
||||
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
|
||||
{
|
||||
tfsAutoSync: true,
|
||||
tfsSyncInterval: 'hourly',
|
||||
} as any
|
||||
);
|
||||
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
organizationUrl: 'https://dev.azure.com/test',
|
||||
personalAccessToken: 'token123',
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
|
||||
expect(scheduler.isActive()).toBe(true);
|
||||
});
|
||||
|
||||
it('ne devrait pas démarrer si désactivé', async () => {
|
||||
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
|
||||
{
|
||||
tfsAutoSync: false,
|
||||
tfsSyncInterval: 'hourly',
|
||||
} as any
|
||||
);
|
||||
|
||||
await scheduler.start('user1');
|
||||
|
||||
expect(scheduler.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('ne devrait pas démarrer si TFS non configuré', async () => {
|
||||
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
|
||||
{
|
||||
tfsAutoSync: true,
|
||||
tfsSyncInterval: 'hourly',
|
||||
} as any
|
||||
);
|
||||
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
|
||||
enabled: false,
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
|
||||
expect(scheduler.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('ne devrait pas démarrer deux fois', async () => {
|
||||
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
|
||||
{
|
||||
tfsAutoSync: true,
|
||||
tfsSyncInterval: 'hourly',
|
||||
} as any
|
||||
);
|
||||
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
organizationUrl: 'https://dev.azure.com/test',
|
||||
personalAccessToken: 'token123',
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
const firstCall = scheduler.isActive();
|
||||
await scheduler.start('user1');
|
||||
const secondCall = scheduler.isActive();
|
||||
|
||||
expect(firstCall).toBe(true);
|
||||
expect(secondCall).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stop', () => {
|
||||
it('devrait arrêter le scheduler', async () => {
|
||||
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
|
||||
{
|
||||
tfsAutoSync: true,
|
||||
tfsSyncInterval: 'hourly',
|
||||
} as any
|
||||
);
|
||||
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
organizationUrl: 'https://dev.azure.com/test',
|
||||
personalAccessToken: 'token123',
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
expect(scheduler.isActive()).toBe(true);
|
||||
|
||||
scheduler.stop();
|
||||
expect(scheduler.isActive()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restart', () => {
|
||||
it('devrait redémarrer le scheduler', async () => {
|
||||
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
|
||||
{
|
||||
tfsAutoSync: true,
|
||||
tfsSyncInterval: 'hourly',
|
||||
} as any
|
||||
);
|
||||
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
organizationUrl: 'https://dev.azure.com/test',
|
||||
personalAccessToken: 'token123',
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
expect(scheduler.isActive()).toBe(true);
|
||||
|
||||
await scheduler.restart('user1');
|
||||
expect(scheduler.isActive()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isActive', () => {
|
||||
it('devrait retourner false si pas démarré', () => {
|
||||
expect(scheduler.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('devrait retourner true si démarré', async () => {
|
||||
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
|
||||
{
|
||||
tfsAutoSync: true,
|
||||
tfsSyncInterval: 'hourly',
|
||||
} as any
|
||||
);
|
||||
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
organizationUrl: 'https://dev.azure.com/test',
|
||||
personalAccessToken: 'token123',
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
expect(scheduler.isActive()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('performScheduledSync', () => {
|
||||
it('devrait exécuter une synchronisation automatique', async () => {
|
||||
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
|
||||
{
|
||||
tfsAutoSync: true,
|
||||
tfsSyncInterval: 'hourly',
|
||||
} as any
|
||||
);
|
||||
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
organizationUrl: 'https://dev.azure.com/test',
|
||||
personalAccessToken: 'token123',
|
||||
} as any);
|
||||
vi.mocked(prisma.user.findFirst).mockResolvedValue({
|
||||
id: 'user1',
|
||||
} as any);
|
||||
vi.mocked(mockTfsService.testConnection).mockResolvedValue(true);
|
||||
vi.mocked(mockTfsService.syncTasks).mockResolvedValue({
|
||||
success: true,
|
||||
pullRequestsCreated: 5,
|
||||
pullRequestsUpdated: 3,
|
||||
pullRequestsSkipped: 1,
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
|
||||
// Avancer le timer pour déclencher la synchronisation
|
||||
vi.advanceTimersByTime(60 * 60 * 1000); // 1 heure
|
||||
|
||||
// Attendre que la synchronisation soit exécutée
|
||||
await vi.waitFor(() => {
|
||||
expect(mockTfsService.syncTasks).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
scheduler.stop();
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs de connexion', async () => {
|
||||
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
|
||||
{
|
||||
tfsAutoSync: true,
|
||||
tfsSyncInterval: 'hourly',
|
||||
} as any
|
||||
);
|
||||
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
organizationUrl: 'https://dev.azure.com/test',
|
||||
personalAccessToken: 'token123',
|
||||
} as any);
|
||||
vi.mocked(mockTfsService.testConnection).mockResolvedValue(false);
|
||||
|
||||
await scheduler.start('user1');
|
||||
|
||||
vi.advanceTimersByTime(60 * 60 * 1000);
|
||||
await vi.waitFor(() => {
|
||||
expect(mockTfsService.testConnection).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
scheduler.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextSyncTime', () => {
|
||||
it('devrait retourner null si pas démarré', async () => {
|
||||
const nextTime = await scheduler.getNextSyncTime();
|
||||
|
||||
expect(nextTime).toBeNull();
|
||||
});
|
||||
|
||||
it('devrait calculer le prochain moment de synchronisation', async () => {
|
||||
const mockDate = new Date('2024-01-15T12:00:00Z');
|
||||
vi.mocked(getToday).mockReturnValue(mockDate);
|
||||
vi.mocked(addMinutes).mockReturnValue(new Date('2024-01-15T13:00:00Z'));
|
||||
|
||||
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
|
||||
{
|
||||
tfsAutoSync: true,
|
||||
tfsSyncInterval: 'hourly',
|
||||
} as any
|
||||
);
|
||||
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
organizationUrl: 'https://dev.azure.com/test',
|
||||
personalAccessToken: 'token123',
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
const nextTime = await scheduler.getNextSyncTime();
|
||||
|
||||
expect(nextTime).not.toBeNull();
|
||||
expect(addMinutes).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatus', () => {
|
||||
it('devrait retourner le statut du scheduler', async () => {
|
||||
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
|
||||
{
|
||||
tfsAutoSync: true,
|
||||
tfsSyncInterval: 'daily',
|
||||
} as any
|
||||
);
|
||||
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
organizationUrl: 'https://dev.azure.com/test',
|
||||
personalAccessToken: 'token123',
|
||||
} as any);
|
||||
|
||||
await scheduler.start('user1');
|
||||
const status = await scheduler.getStatus('user1');
|
||||
|
||||
expect(status.isRunning).toBe(true);
|
||||
expect(status.isEnabled).toBe(true);
|
||||
expect(status.interval).toBe('daily');
|
||||
expect(status.tfsConfigured).toBe(true);
|
||||
});
|
||||
|
||||
it('devrait retourner le statut si pas démarré', async () => {
|
||||
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
|
||||
{
|
||||
tfsAutoSync: false,
|
||||
tfsSyncInterval: 'hourly',
|
||||
} as any
|
||||
);
|
||||
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
|
||||
enabled: false,
|
||||
} as any);
|
||||
|
||||
const status = await scheduler.getStatus('user1');
|
||||
|
||||
expect(status.isRunning).toBe(false);
|
||||
expect(status.isEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
71
src/services/integrations/tfs/__tests__/tfs.test.ts
Normal file
71
src/services/integrations/tfs/__tests__/tfs.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Tests unitaires pour TfsService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { TfsService } from '../tfs';
|
||||
|
||||
// Mock de fetch global
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe('TfsService', () => {
|
||||
let service: TfsService;
|
||||
|
||||
const mockConfig = {
|
||||
enabled: true,
|
||||
organizationUrl: 'https://dev.azure.com/test',
|
||||
personalAccessToken: 'token123',
|
||||
repositories: [],
|
||||
ignoredRepositories: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
service = new TfsService(mockConfig);
|
||||
});
|
||||
|
||||
describe('testConnection', () => {
|
||||
it('devrait retourner true si la connexion réussit', async () => {
|
||||
vi.mocked(fetch).mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as any);
|
||||
|
||||
const result = await service.testConnection();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(fetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait retourner false si authentification échoue', async () => {
|
||||
vi.mocked(fetch).mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
} as any);
|
||||
|
||||
const result = await service.testConnection();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('devrait retourner false si accès refusé', async () => {
|
||||
vi.mocked(fetch).mockResolvedValue({
|
||||
ok: false,
|
||||
status: 403,
|
||||
} as any);
|
||||
|
||||
const result = await service.testConnection();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('devrait gérer les erreurs de réseau', async () => {
|
||||
vi.mocked(fetch).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const result = await service.testConnection();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
309
src/services/task-management/__tests__/daily.test.ts
Normal file
309
src/services/task-management/__tests__/daily.test.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Tests unitaires pour DailyService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { DailyService } from '../daily';
|
||||
import { prisma } from '@/services/core/database';
|
||||
import {
|
||||
getPreviousWorkday,
|
||||
normalizeDate,
|
||||
getToday,
|
||||
getYesterday,
|
||||
} from '@/lib/date-utils';
|
||||
|
||||
// Mock de prisma
|
||||
vi.mock('@/services/core/database', () => ({
|
||||
prisma: {
|
||||
dailyCheckbox: {
|
||||
findMany: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
aggregate: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock de date-utils
|
||||
vi.mock('@/lib/date-utils', () => ({
|
||||
getPreviousWorkday: vi.fn(),
|
||||
normalizeDate: vi.fn(),
|
||||
formatDateForAPI: vi.fn(),
|
||||
getToday: vi.fn(),
|
||||
getYesterday: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('DailyService', () => {
|
||||
let service: DailyService;
|
||||
const mockUserId = 'user-123';
|
||||
const mockDate = new Date('2024-01-15');
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
service = new DailyService();
|
||||
vi.mocked(normalizeDate).mockImplementation((date) => date);
|
||||
vi.mocked(getPreviousWorkday).mockReturnValue(new Date('2024-01-14'));
|
||||
vi.mocked(getToday).mockReturnValue(mockDate);
|
||||
vi.mocked(getYesterday).mockReturnValue(new Date('2024-01-14'));
|
||||
});
|
||||
|
||||
describe('getDailyView', () => {
|
||||
it('devrait récupérer la vue daily pour une date', async () => {
|
||||
const yesterdayCheckboxes: any[] = [];
|
||||
const todayCheckboxes: any[] = [];
|
||||
|
||||
vi.mocked(prisma.dailyCheckbox.findMany)
|
||||
.mockResolvedValueOnce(yesterdayCheckboxes)
|
||||
.mockResolvedValueOnce(todayCheckboxes);
|
||||
|
||||
const view = await service.getDailyView(mockDate, mockUserId);
|
||||
|
||||
expect(view.date).toEqual(mockDate);
|
||||
expect(view.yesterday).toEqual(yesterdayCheckboxes);
|
||||
expect(view.today).toEqual(todayCheckboxes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCheckboxesByDate', () => {
|
||||
it('devrait récupérer les checkboxes pour une date', async () => {
|
||||
const mockCheckboxes = [
|
||||
{
|
||||
id: 'checkbox-1',
|
||||
date: mockDate,
|
||||
text: 'Checkbox 1',
|
||||
isChecked: false,
|
||||
type: 'task',
|
||||
order: 0,
|
||||
taskId: null,
|
||||
userId: mockUserId,
|
||||
task: null,
|
||||
user: { id: mockUserId },
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue(
|
||||
mockCheckboxes as any
|
||||
);
|
||||
|
||||
const checkboxes = await service.getCheckboxesByDate(
|
||||
mockDate,
|
||||
mockUserId
|
||||
);
|
||||
|
||||
expect(checkboxes).toBeDefined();
|
||||
expect(prisma.dailyCheckbox.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
date: mockDate,
|
||||
userId: mockUserId,
|
||||
},
|
||||
include: {
|
||||
task: {
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
primaryTag: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
orderBy: { order: 'asc' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addCheckbox', () => {
|
||||
it('devrait ajouter une checkbox', async () => {
|
||||
const mockCheckbox = {
|
||||
id: 'checkbox-1',
|
||||
date: mockDate,
|
||||
text: 'New checkbox',
|
||||
isChecked: false,
|
||||
type: 'task',
|
||||
order: 0,
|
||||
taskId: null,
|
||||
userId: mockUserId,
|
||||
task: null,
|
||||
user: { id: mockUserId },
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
vi.mocked(prisma.dailyCheckbox.aggregate).mockResolvedValue({
|
||||
_max: { order: null },
|
||||
} as any);
|
||||
vi.mocked(prisma.dailyCheckbox.create).mockResolvedValue(
|
||||
mockCheckbox as any
|
||||
);
|
||||
|
||||
const checkbox = await service.addCheckbox({
|
||||
date: mockDate,
|
||||
text: 'New checkbox',
|
||||
userId: mockUserId,
|
||||
});
|
||||
|
||||
expect(checkbox).toBeDefined();
|
||||
expect(prisma.dailyCheckbox.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("devrait utiliser l'ordre spécifié", async () => {
|
||||
const mockCheckbox = {
|
||||
id: 'checkbox-1',
|
||||
date: mockDate,
|
||||
text: 'New checkbox',
|
||||
isChecked: false,
|
||||
type: 'task',
|
||||
order: 5,
|
||||
taskId: null,
|
||||
userId: mockUserId,
|
||||
task: null,
|
||||
user: { id: mockUserId },
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
vi.mocked(prisma.dailyCheckbox.create).mockResolvedValue(
|
||||
mockCheckbox as any
|
||||
);
|
||||
|
||||
await service.addCheckbox({
|
||||
date: mockDate,
|
||||
text: 'New checkbox',
|
||||
userId: mockUserId,
|
||||
order: 5,
|
||||
});
|
||||
|
||||
expect(prisma.dailyCheckbox.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
order: 5,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateCheckbox', () => {
|
||||
it('devrait mettre à jour une checkbox', async () => {
|
||||
const mockCheckbox = {
|
||||
id: 'checkbox-1',
|
||||
date: mockDate,
|
||||
text: 'Updated checkbox',
|
||||
isChecked: true,
|
||||
type: 'task',
|
||||
order: 0,
|
||||
taskId: null,
|
||||
userId: mockUserId,
|
||||
task: null,
|
||||
user: { id: mockUserId },
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
vi.mocked(prisma.dailyCheckbox.update).mockResolvedValue(
|
||||
mockCheckbox as any
|
||||
);
|
||||
|
||||
const checkbox = await service.updateCheckbox('checkbox-1', {
|
||||
text: 'Updated checkbox',
|
||||
isChecked: true,
|
||||
});
|
||||
|
||||
expect(checkbox).toBeDefined();
|
||||
expect(prisma.dailyCheckbox.update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleCheckbox', () => {
|
||||
it("devrait toggle l'état d'une checkbox", async () => {
|
||||
const existingCheckbox = {
|
||||
id: 'checkbox-1',
|
||||
isChecked: false,
|
||||
};
|
||||
|
||||
const updatedCheckbox = {
|
||||
id: 'checkbox-1',
|
||||
date: mockDate,
|
||||
text: 'Checkbox',
|
||||
isChecked: true,
|
||||
type: 'task',
|
||||
order: 0,
|
||||
taskId: null,
|
||||
userId: mockUserId,
|
||||
task: null,
|
||||
user: { id: mockUserId },
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
vi.mocked(prisma.dailyCheckbox.findUnique).mockResolvedValue(
|
||||
existingCheckbox as any
|
||||
);
|
||||
vi.mocked(prisma.dailyCheckbox.update).mockResolvedValue(
|
||||
updatedCheckbox as any
|
||||
);
|
||||
|
||||
const checkbox = await service.toggleCheckbox('checkbox-1');
|
||||
|
||||
expect(checkbox.isChecked).toBe(true);
|
||||
expect(prisma.dailyCheckbox.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
isChecked: true,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("devrait lancer une erreur si la checkbox n'existe pas", async () => {
|
||||
vi.mocked(prisma.dailyCheckbox.findUnique).mockResolvedValue(null);
|
||||
|
||||
await expect(service.toggleCheckbox('non-existent')).rejects.toThrow(
|
||||
'non trouvée'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteCheckbox', () => {
|
||||
it('devrait supprimer une checkbox', async () => {
|
||||
const existingCheckbox = {
|
||||
id: 'checkbox-1',
|
||||
date: mockDate,
|
||||
text: 'Checkbox',
|
||||
isChecked: false,
|
||||
type: 'task',
|
||||
order: 0,
|
||||
taskId: null,
|
||||
userId: mockUserId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
vi.mocked(prisma.dailyCheckbox.findUnique).mockResolvedValue(
|
||||
existingCheckbox as any
|
||||
);
|
||||
vi.mocked(prisma.dailyCheckbox.delete).mockResolvedValue({} as any);
|
||||
|
||||
await service.deleteCheckbox('checkbox-1');
|
||||
|
||||
expect(prisma.dailyCheckbox.delete).toHaveBeenCalledWith({
|
||||
where: { id: 'checkbox-1' },
|
||||
});
|
||||
});
|
||||
|
||||
it("devrait lancer une erreur si la checkbox n'existe pas", async () => {
|
||||
vi.mocked(prisma.dailyCheckbox.findUnique).mockResolvedValue(null);
|
||||
|
||||
await expect(service.deleteCheckbox('non-existent')).rejects.toThrow(
|
||||
'non trouvée'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
286
src/services/task-management/__tests__/tags.test.ts
Normal file
286
src/services/task-management/__tests__/tags.test.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Tests unitaires pour tagsService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { tagsService } from '../tags';
|
||||
import { prisma } from '@/services/core/database';
|
||||
|
||||
// Mock de prisma
|
||||
vi.mock('@/services/core/database', () => ({
|
||||
prisma: {
|
||||
tag: {
|
||||
findMany: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
taskTag: {
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('tagsService', () => {
|
||||
const mockUserId = 'user-123';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getTags', () => {
|
||||
it('devrait récupérer tous les tags avec leur usage', async () => {
|
||||
const mockTags = [
|
||||
{
|
||||
id: 'tag-1',
|
||||
name: 'Tag 1',
|
||||
color: '#ff0000',
|
||||
isPinned: false,
|
||||
_count: { taskTags: 5 },
|
||||
},
|
||||
{
|
||||
id: 'tag-2',
|
||||
name: 'Tag 2',
|
||||
color: '#00ff00',
|
||||
isPinned: true,
|
||||
_count: { taskTags: 2 },
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.tag.findMany).mockResolvedValue(mockTags as any);
|
||||
|
||||
const tags = await tagsService.getTags(mockUserId);
|
||||
|
||||
expect(tags).toHaveLength(2);
|
||||
expect(tags[0]).toEqual({
|
||||
id: 'tag-1',
|
||||
name: 'Tag 1',
|
||||
color: '#ff0000',
|
||||
isPinned: false,
|
||||
usage: 5,
|
||||
});
|
||||
expect(prisma.tag.findMany).toHaveBeenCalledWith({
|
||||
where: { ownerId: mockUserId },
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
taskTags: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagById', () => {
|
||||
it('devrait récupérer un tag par son ID', async () => {
|
||||
const mockTag = {
|
||||
id: 'tag-1',
|
||||
name: 'Tag 1',
|
||||
color: '#ff0000',
|
||||
isPinned: false,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.tag.findFirst).mockResolvedValue(mockTag as any);
|
||||
|
||||
const tag = await tagsService.getTagById('tag-1', mockUserId);
|
||||
|
||||
expect(tag).toEqual(mockTag);
|
||||
expect(prisma.tag.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: 'tag-1',
|
||||
ownerId: mockUserId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("devrait retourner null si le tag n'existe pas", async () => {
|
||||
vi.mocked(prisma.tag.findFirst).mockResolvedValue(null);
|
||||
|
||||
const tag = await tagsService.getTagById('non-existent', mockUserId);
|
||||
|
||||
expect(tag).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagByName', () => {
|
||||
it('devrait récupérer un tag par son nom', async () => {
|
||||
const mockTag = {
|
||||
id: 'tag-1',
|
||||
name: 'Tag 1',
|
||||
color: '#ff0000',
|
||||
isPinned: false,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.tag.findFirst).mockResolvedValue(mockTag as any);
|
||||
|
||||
const tag = await tagsService.getTagByName('Tag 1', mockUserId);
|
||||
|
||||
expect(tag).toEqual(mockTag);
|
||||
expect(prisma.tag.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
name: {
|
||||
equals: 'Tag 1',
|
||||
},
|
||||
ownerId: mockUserId,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTag', () => {
|
||||
it('devrait créer un nouveau tag', async () => {
|
||||
const mockTag = {
|
||||
id: 'tag-1',
|
||||
name: 'New Tag',
|
||||
color: '#ff0000',
|
||||
isPinned: false,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.tag.findFirst).mockResolvedValue(null); // Pas de tag existant
|
||||
vi.mocked(prisma.tag.create).mockResolvedValue(mockTag as any);
|
||||
|
||||
const tag = await tagsService.createTag({
|
||||
name: 'New Tag',
|
||||
color: '#ff0000',
|
||||
userId: mockUserId,
|
||||
});
|
||||
|
||||
expect(tag).toEqual(mockTag);
|
||||
expect(prisma.tag.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
name: 'New Tag',
|
||||
color: '#ff0000',
|
||||
isPinned: false,
|
||||
ownerId: mockUserId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('devrait lancer une erreur si le tag existe déjà', async () => {
|
||||
const existingTag = {
|
||||
id: 'tag-1',
|
||||
name: 'Existing Tag',
|
||||
color: '#ff0000',
|
||||
isPinned: false,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.tag.findFirst).mockResolvedValue(existingTag as any);
|
||||
|
||||
await expect(
|
||||
tagsService.createTag({
|
||||
name: 'Existing Tag',
|
||||
color: '#ff0000',
|
||||
userId: mockUserId,
|
||||
})
|
||||
).rejects.toThrow('existe déjà');
|
||||
});
|
||||
|
||||
it('devrait trimmer le nom du tag', async () => {
|
||||
const mockTag = {
|
||||
id: 'tag-1',
|
||||
name: 'Trimmed Tag',
|
||||
color: '#ff0000',
|
||||
isPinned: false,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.tag.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(prisma.tag.create).mockResolvedValue(mockTag as any);
|
||||
|
||||
await tagsService.createTag({
|
||||
name: ' Trimmed Tag ',
|
||||
color: '#ff0000',
|
||||
userId: mockUserId,
|
||||
});
|
||||
|
||||
expect(prisma.tag.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
name: 'Trimmed Tag',
|
||||
color: '#ff0000',
|
||||
isPinned: false,
|
||||
ownerId: mockUserId,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTag', () => {
|
||||
it('devrait mettre à jour un tag', async () => {
|
||||
const mockTag = {
|
||||
id: 'tag-1',
|
||||
name: 'Updated Tag',
|
||||
color: '#00ff00',
|
||||
isPinned: true,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.tag.findFirst).mockResolvedValue({
|
||||
id: 'tag-1',
|
||||
name: 'Old Tag',
|
||||
color: '#ff0000',
|
||||
isPinned: false,
|
||||
} as any);
|
||||
vi.mocked(prisma.tag.update).mockResolvedValue(mockTag as any);
|
||||
|
||||
const tag = await tagsService.updateTag('tag-1', mockUserId, {
|
||||
name: 'Updated Tag',
|
||||
color: '#00ff00',
|
||||
isPinned: true,
|
||||
});
|
||||
|
||||
expect(tag).toEqual(mockTag);
|
||||
expect(prisma.tag.update).toHaveBeenCalledWith({
|
||||
where: { id: 'tag-1' },
|
||||
data: {
|
||||
name: 'Updated Tag',
|
||||
color: '#00ff00',
|
||||
isPinned: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("devrait lancer une erreur si le tag n'existe pas", async () => {
|
||||
vi.mocked(prisma.tag.findFirst).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
tagsService.updateTag('non-existent', mockUserId, {
|
||||
name: 'Updated Tag',
|
||||
})
|
||||
).rejects.toThrow('non trouvé');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteTag', () => {
|
||||
it('devrait supprimer un tag', async () => {
|
||||
vi.mocked(prisma.tag.findFirst).mockResolvedValue({
|
||||
id: 'tag-1',
|
||||
name: 'Tag to delete',
|
||||
color: '#ff0000',
|
||||
isPinned: false,
|
||||
} as any);
|
||||
vi.mocked(prisma.taskTag.deleteMany).mockResolvedValue({
|
||||
count: 0,
|
||||
} as any);
|
||||
vi.mocked(prisma.tag.delete).mockResolvedValue({} as any);
|
||||
|
||||
await tagsService.deleteTag('tag-1', mockUserId);
|
||||
|
||||
expect(prisma.taskTag.deleteMany).toHaveBeenCalledWith({
|
||||
where: { tagId: 'tag-1' },
|
||||
});
|
||||
expect(prisma.tag.delete).toHaveBeenCalledWith({
|
||||
where: { id: 'tag-1' },
|
||||
});
|
||||
});
|
||||
|
||||
it("devrait lancer une erreur si le tag n'existe pas", async () => {
|
||||
vi.mocked(prisma.tag.findFirst).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
tagsService.deleteTag('non-existent', mockUserId)
|
||||
).rejects.toThrow('non trouvé');
|
||||
});
|
||||
});
|
||||
});
|
||||
436
src/services/task-management/__tests__/tasks.test.ts
Normal file
436
src/services/task-management/__tests__/tasks.test.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
/**
|
||||
* Tests unitaires pour TasksService
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { TasksService } from '../tasks';
|
||||
import { prisma } from '@/services/core/database';
|
||||
import { tagsService } from '../tags';
|
||||
import { getReadonlyFieldsForTask } from '../readonly-fields';
|
||||
import { getToday } from '@/lib/date-utils';
|
||||
|
||||
// Mock de prisma
|
||||
vi.mock('@/services/core/database', () => ({
|
||||
prisma: {
|
||||
task: {
|
||||
findMany: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
aggregate: vi.fn(),
|
||||
},
|
||||
taskTag: {
|
||||
createMany: vi.fn(),
|
||||
deleteMany: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
dailyCheckbox: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock de tagsService
|
||||
vi.mock('../tags', () => ({
|
||||
tagsService: {
|
||||
ensureTagsExist: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock de readonly-fields
|
||||
vi.mock('../readonly-fields', () => ({
|
||||
getReadonlyFieldsForTask: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock de date-utils
|
||||
vi.mock('@/lib/date-utils', () => ({
|
||||
getToday: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('TasksService', () => {
|
||||
let service: TasksService;
|
||||
const mockUserId = 'user-123';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
service = new TasksService();
|
||||
vi.mocked(getToday).mockReturnValue(new Date('2024-01-15'));
|
||||
});
|
||||
|
||||
describe('getTasks', () => {
|
||||
it('devrait récupérer toutes les tâches pour un utilisateur', async () => {
|
||||
const mockTasks = [
|
||||
{
|
||||
id: 'task-1',
|
||||
title: 'Task 1',
|
||||
description: null,
|
||||
status: 'todo',
|
||||
priority: 'medium',
|
||||
source: 'manual',
|
||||
sourceId: 'manual-123',
|
||||
ownerId: mockUserId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
dueDate: null,
|
||||
completedAt: null,
|
||||
primaryTagId: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
tfsProject: null,
|
||||
tfsPullRequestId: null,
|
||||
tfsRepository: null,
|
||||
tfsSourceBranch: null,
|
||||
tfsTargetBranch: null,
|
||||
assignee: null,
|
||||
taskTags: [],
|
||||
primaryTag: null,
|
||||
_count: { dailyCheckboxes: 0 },
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any);
|
||||
vi.mocked(getReadonlyFieldsForTask).mockReturnValue([]);
|
||||
|
||||
const tasks = await service.getTasks(mockUserId);
|
||||
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(prisma.task.findMany).toHaveBeenCalledWith({
|
||||
where: { ownerId: mockUserId },
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
primaryTag: true,
|
||||
_count: {
|
||||
select: {
|
||||
dailyCheckboxes: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
take: undefined,
|
||||
skip: 0,
|
||||
orderBy: [
|
||||
{ completedAt: 'desc' },
|
||||
{ dueDate: 'asc' },
|
||||
{ createdAt: 'desc' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('devrait filtrer par statut', async () => {
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue([]);
|
||||
|
||||
await service.getTasks(mockUserId, { status: ['todo', 'in_progress'] });
|
||||
|
||||
expect(prisma.task.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
status: { in: ['todo', 'in_progress'] },
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('devrait rechercher dans le titre et la description', async () => {
|
||||
vi.mocked(prisma.task.findMany).mockResolvedValue([]);
|
||||
|
||||
await service.getTasks(mockUserId, { search: 'test' });
|
||||
|
||||
expect(prisma.task.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
OR: [
|
||||
{ title: { contains: 'test' } },
|
||||
{ description: { contains: 'test' } },
|
||||
],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTask', () => {
|
||||
it('devrait créer une nouvelle tâche', async () => {
|
||||
const mockTask = {
|
||||
id: 'task-1',
|
||||
title: 'New Task',
|
||||
description: 'Description',
|
||||
status: 'todo',
|
||||
priority: 'medium',
|
||||
ownerId: mockUserId,
|
||||
source: 'manual',
|
||||
sourceId: 'manual-123',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
dueDate: null,
|
||||
completedAt: null,
|
||||
primaryTagId: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
tfsProject: null,
|
||||
tfsPullRequestId: null,
|
||||
tfsRepository: null,
|
||||
tfsSourceBranch: null,
|
||||
tfsTargetBranch: null,
|
||||
assignee: null,
|
||||
taskTags: [],
|
||||
primaryTag: null,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.task.create).mockResolvedValue(mockTask as any);
|
||||
vi.mocked(prisma.task.findUnique).mockResolvedValue(mockTask as any);
|
||||
vi.mocked(tagsService.ensureTagsExist).mockResolvedValue([]);
|
||||
vi.mocked(getReadonlyFieldsForTask).mockReturnValue([]);
|
||||
|
||||
const task = await service.createTask({
|
||||
title: 'New Task',
|
||||
description: 'Description',
|
||||
ownerId: mockUserId,
|
||||
});
|
||||
|
||||
expect(task).toBeDefined();
|
||||
expect(prisma.task.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait définir completedAt si le statut est done', async () => {
|
||||
const mockTask = {
|
||||
id: 'task-1',
|
||||
title: 'Done Task',
|
||||
description: null,
|
||||
status: 'done',
|
||||
priority: 'medium',
|
||||
ownerId: mockUserId,
|
||||
source: 'manual',
|
||||
sourceId: 'manual-123',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
dueDate: null,
|
||||
completedAt: new Date('2024-01-15'),
|
||||
primaryTagId: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
tfsProject: null,
|
||||
tfsPullRequestId: null,
|
||||
tfsRepository: null,
|
||||
tfsSourceBranch: null,
|
||||
tfsTargetBranch: null,
|
||||
assignee: null,
|
||||
taskTags: [],
|
||||
primaryTag: null,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.task.create).mockResolvedValue(mockTask as any);
|
||||
vi.mocked(prisma.task.findUnique).mockResolvedValue(mockTask as any);
|
||||
vi.mocked(tagsService.ensureTagsExist).mockResolvedValue([]);
|
||||
vi.mocked(getReadonlyFieldsForTask).mockReturnValue([]);
|
||||
|
||||
await service.createTask({
|
||||
title: 'Done Task',
|
||||
status: 'done',
|
||||
ownerId: mockUserId,
|
||||
});
|
||||
|
||||
expect(prisma.task.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
completedAt: expect.any(Date),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTask', () => {
|
||||
it('devrait mettre à jour une tâche', async () => {
|
||||
const mockTask = {
|
||||
id: 'task-1',
|
||||
title: 'Updated Task',
|
||||
description: null,
|
||||
status: 'in_progress',
|
||||
priority: 'medium',
|
||||
ownerId: mockUserId,
|
||||
source: 'manual',
|
||||
sourceId: 'manual-123',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
dueDate: null,
|
||||
completedAt: null,
|
||||
primaryTagId: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
tfsProject: null,
|
||||
tfsPullRequestId: null,
|
||||
tfsRepository: null,
|
||||
tfsSourceBranch: null,
|
||||
tfsTargetBranch: null,
|
||||
assignee: null,
|
||||
taskTags: [],
|
||||
primaryTag: null,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.task.findFirst).mockResolvedValue(mockTask as any);
|
||||
vi.mocked(prisma.task.update).mockResolvedValue(mockTask as any);
|
||||
vi.mocked(prisma.task.findUnique).mockResolvedValue(mockTask as any);
|
||||
vi.mocked(getReadonlyFieldsForTask).mockReturnValue([]);
|
||||
|
||||
const task = await service.updateTask(mockUserId, 'task-1', {
|
||||
title: 'Updated Task',
|
||||
status: 'in_progress',
|
||||
});
|
||||
|
||||
expect(task).toBeDefined();
|
||||
expect(prisma.task.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('devrait définir completedAt quand le statut passe à done', async () => {
|
||||
const mockTask = {
|
||||
id: 'task-1',
|
||||
title: 'Task',
|
||||
description: null,
|
||||
status: 'todo',
|
||||
priority: 'medium',
|
||||
ownerId: mockUserId,
|
||||
source: 'manual',
|
||||
sourceId: 'manual-123',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
dueDate: null,
|
||||
completedAt: null,
|
||||
primaryTagId: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
tfsProject: null,
|
||||
tfsPullRequestId: null,
|
||||
tfsRepository: null,
|
||||
tfsSourceBranch: null,
|
||||
tfsTargetBranch: null,
|
||||
assignee: null,
|
||||
taskTags: [],
|
||||
primaryTag: null,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.task.findFirst).mockResolvedValue(mockTask as any);
|
||||
vi.mocked(prisma.task.update).mockResolvedValue({
|
||||
...mockTask,
|
||||
status: 'done',
|
||||
completedAt: new Date('2024-01-15'),
|
||||
} as any);
|
||||
vi.mocked(prisma.task.findUnique).mockResolvedValue({
|
||||
...mockTask,
|
||||
status: 'done',
|
||||
completedAt: new Date('2024-01-15'),
|
||||
} as any);
|
||||
vi.mocked(getReadonlyFieldsForTask).mockReturnValue([]);
|
||||
|
||||
await service.updateTask(mockUserId, 'task-1', {
|
||||
status: 'done',
|
||||
});
|
||||
|
||||
expect(prisma.task.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
completedAt: expect.any(Date),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("devrait lancer une erreur si la tâche n'existe pas", async () => {
|
||||
vi.mocked(prisma.task.findFirst).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.updateTask(mockUserId, 'non-existent', {
|
||||
title: 'Updated',
|
||||
})
|
||||
).rejects.toThrow('introuvable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteTask', () => {
|
||||
it('devrait supprimer une tâche', async () => {
|
||||
const mockTask = {
|
||||
id: 'task-1',
|
||||
title: 'Task to delete',
|
||||
ownerId: mockUserId,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.task.findFirst).mockResolvedValue(mockTask as any);
|
||||
vi.mocked(prisma.task.delete).mockResolvedValue({} as any);
|
||||
|
||||
await service.deleteTask(mockUserId, 'task-1');
|
||||
|
||||
expect(prisma.task.delete).toHaveBeenCalledWith({
|
||||
where: { id: 'task-1' },
|
||||
});
|
||||
});
|
||||
|
||||
it("devrait lancer une erreur si la tâche n'existe pas", async () => {
|
||||
vi.mocked(prisma.task.findFirst).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.deleteTask(mockUserId, 'non-existent')
|
||||
).rejects.toThrow('introuvable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTaskStatus', () => {
|
||||
it("devrait mettre à jour le statut d'une tâche", async () => {
|
||||
const mockTask = {
|
||||
id: 'task-1',
|
||||
title: 'Task',
|
||||
description: null,
|
||||
status: 'todo',
|
||||
priority: 'medium',
|
||||
ownerId: mockUserId,
|
||||
source: 'manual',
|
||||
sourceId: 'manual-123',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
dueDate: null,
|
||||
completedAt: null,
|
||||
primaryTagId: null,
|
||||
jiraProject: null,
|
||||
jiraKey: null,
|
||||
jiraType: null,
|
||||
tfsProject: null,
|
||||
tfsPullRequestId: null,
|
||||
tfsRepository: null,
|
||||
tfsSourceBranch: null,
|
||||
tfsTargetBranch: null,
|
||||
assignee: null,
|
||||
taskTags: [],
|
||||
primaryTag: null,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.task.findFirst).mockResolvedValue(mockTask as any);
|
||||
vi.mocked(prisma.task.update).mockResolvedValue({
|
||||
...mockTask,
|
||||
status: 'in_progress',
|
||||
} as any);
|
||||
vi.mocked(prisma.task.findUnique).mockResolvedValue({
|
||||
...mockTask,
|
||||
status: 'in_progress',
|
||||
} as any);
|
||||
vi.mocked(getReadonlyFieldsForTask).mockReturnValue([]);
|
||||
|
||||
const task = await service.updateTaskStatus(
|
||||
mockUserId,
|
||||
'task-1',
|
||||
'in_progress'
|
||||
);
|
||||
|
||||
expect(task).toBeDefined();
|
||||
expect(prisma.task.update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user