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",
|
"emoji-regex": "^10.5.0",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
"mermaid": "^11.12.0",
|
"mermaid": "^11.12.0",
|
||||||
"next": "15.5.3",
|
"next": "15.5.7",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.12",
|
||||||
"prism-react-renderer": "^2.4.1",
|
"prism-react-renderer": "^2.4.1",
|
||||||
"prisma": "^6.16.1",
|
"prisma": "^6.16.1",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
@@ -60,24 +60,30 @@
|
|||||||
"twemoji": "^14.0.2"
|
"twemoji": "^14.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3.3.3",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "^15.5.3",
|
"eslint-config-next": "^15.5.7",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"knip": "^5.64.0",
|
"knip": "^5.71.0",
|
||||||
"lint-staged": "^15.5.2",
|
"lint-staged": "^15.5.2",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.17",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"vitest": "^2.1.8"
|
"vitest": "^2.1.8"
|
||||||
},
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"esbuild": ">=0.25.0",
|
||||||
|
"mdast-util-to-hast": ">=13.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,jsx,ts,tsx,json,css,md}": [
|
"*.{js,jsx,ts,tsx,json,css,md}": [
|
||||||
"prettier --write"
|
"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';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { ManagerSummary } from '@/services/analytics/manager-summary';
|
import { ManagerSummary } from '@/services/analytics/manager-summary';
|
||||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Tabs, TabItem } from '@/components/ui/Tabs';
|
import { Tabs, TabItem } from '@/components/ui/Tabs';
|
||||||
import { AchievementCard } from '@/components/ui/AchievementCard';
|
import { TaskCard } from '@/components/ui/TaskCard';
|
||||||
import { ChallengeCard } from '@/components/ui/ChallengeCard';
|
import { FilterChip } from '@/components/ui/FilterChip';
|
||||||
import { useTasksContext } from '@/contexts/TasksContext';
|
import { useTasksContext } from '@/contexts/TasksContext';
|
||||||
import { MetricsTab } from './MetricsTab';
|
import { MetricsTab } from './MetricsTab';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
@@ -22,15 +22,19 @@ export default function ManagerWeeklySummary({
|
|||||||
initialSummary,
|
initialSummary,
|
||||||
}: ManagerWeeklySummaryProps) {
|
}: ManagerWeeklySummaryProps) {
|
||||||
const [summary] = useState<ManagerSummary>(initialSummary);
|
const [summary] = useState<ManagerSummary>(initialSummary);
|
||||||
const [activeView, setActiveView] = useState<
|
const [activeView, setActiveView] = useState<'narrative' | 'metrics'>(
|
||||||
'narrative' | 'accomplishments' | 'challenges' | 'metrics'
|
'narrative'
|
||||||
>('narrative');
|
);
|
||||||
|
const [selectedAccomplishmentTags, setSelectedAccomplishmentTags] = useState<
|
||||||
|
string[]
|
||||||
|
>([]);
|
||||||
|
const [selectedChallengeTags, setSelectedChallengeTags] = useState<string[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
const { tags: availableTags } = useTasksContext();
|
const { tags: availableTags } = useTasksContext();
|
||||||
|
|
||||||
const handleTabChange = (tabId: string) => {
|
const handleTabChange = (tabId: string) => {
|
||||||
setActiveView(
|
setActiveView(tabId as 'narrative' | 'metrics');
|
||||||
tabId as 'narrative' | 'accomplishments' | 'challenges' | 'metrics'
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
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 })})`;
|
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
|
// Configuration des onglets
|
||||||
const tabItems: TabItem[] = [
|
const tabItems: TabItem[] = [
|
||||||
{ id: 'narrative', label: 'Vue Executive', icon: '📝' },
|
{ 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: '📊' },
|
{ id: 'metrics', label: 'Métriques', icon: '📊' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -197,7 +279,7 @@ export default function ManagerWeeklySummary({
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Top accomplissements */}
|
{/* Accomplissements détaillés */}
|
||||||
<Card variant="elevated">
|
<Card variant="elevated">
|
||||||
<CardHeader
|
<CardHeader
|
||||||
className="pb-4"
|
className="pb-4"
|
||||||
@@ -211,34 +293,98 @@ export default function ManagerWeeklySummary({
|
|||||||
className="text-lg font-semibold flex items-center gap-2"
|
className="text-lg font-semibold flex items-center gap-2"
|
||||||
style={{ color: 'var(--success)' }}
|
style={{ color: 'var(--success)' }}
|
||||||
>
|
>
|
||||||
<Emoji emoji="🏆" /> Top accomplissements
|
<Emoji emoji="🏆" /> Accomplissements
|
||||||
</h2>
|
</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>
|
</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>
|
<CardContent>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<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)]">
|
<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.
|
Aucun accomplissement significatif trouvé cette semaine.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm mt-2">
|
<p className="text-sm">
|
||||||
Ajoutez des tâches avec priorité haute/medium ou des
|
Ajoutez des tâches avec priorité haute/medium ou des
|
||||||
meetings.
|
meetings.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
summary.keyAccomplishments
|
filteredAccomplishments.map((accomplishment) => (
|
||||||
.slice(0, 6)
|
<TaskCard
|
||||||
.map((accomplishment, index) => (
|
|
||||||
<AchievementCard
|
|
||||||
key={accomplishment.id}
|
key={accomplishment.id}
|
||||||
achievement={accomplishment}
|
variant="detailed"
|
||||||
availableTags={
|
title={accomplishment.title}
|
||||||
availableTags as (Tag & { usage: number })[]
|
description={accomplishment.description}
|
||||||
|
tags={accomplishment.tags}
|
||||||
|
priority={
|
||||||
|
accomplishment.impact === 'high'
|
||||||
|
? 'high'
|
||||||
|
: accomplishment.impact === 'medium'
|
||||||
|
? 'medium'
|
||||||
|
: 'low'
|
||||||
}
|
}
|
||||||
index={index}
|
status="done"
|
||||||
showDescription={true}
|
completedAt={accomplishment.completedAt}
|
||||||
maxTags={2}
|
todosCount={accomplishment.todosCount}
|
||||||
|
availableTags={availableTags as Tag[]}
|
||||||
|
source="manual"
|
||||||
|
style={{ opacity: 1 }}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -246,7 +392,7 @@ export default function ManagerWeeklySummary({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Top challenges */}
|
{/* Enjeux à venir détaillés */}
|
||||||
<Card variant="elevated">
|
<Card variant="elevated">
|
||||||
<CardHeader
|
<CardHeader
|
||||||
className="pb-4"
|
className="pb-4"
|
||||||
@@ -260,32 +406,104 @@ export default function ManagerWeeklySummary({
|
|||||||
className="text-lg font-semibold flex items-center gap-2"
|
className="text-lg font-semibold flex items-center gap-2"
|
||||||
style={{ color: 'var(--destructive)' }}
|
style={{ color: 'var(--destructive)' }}
|
||||||
>
|
>
|
||||||
<Emoji emoji="🎯" /> Top enjeux à venir
|
<Emoji emoji="🎯" /> Enjeux à venir
|
||||||
</h2>
|
</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>
|
</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">
|
<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)]">
|
<div className="col-span-3 text-center py-8 text-[var(--muted-foreground)]">
|
||||||
<p>Aucun enjeu prioritaire trouvé.</p>
|
<div className="text-4xl mb-4">
|
||||||
<p className="text-sm mt-2">
|
<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é
|
Ajoutez des tâches non complétées avec priorité
|
||||||
haute/medium.
|
haute/medium.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
summary.upcomingChallenges
|
filteredChallenges.map((challenge) => (
|
||||||
.slice(0, 6)
|
<TaskCard
|
||||||
.map((challenge, index) => (
|
|
||||||
<ChallengeCard
|
|
||||||
key={challenge.id}
|
key={challenge.id}
|
||||||
challenge={challenge}
|
variant="detailed"
|
||||||
availableTags={
|
title={challenge.title}
|
||||||
availableTags as (Tag & { usage: number })[]
|
description={challenge.description}
|
||||||
}
|
tags={challenge.tags}
|
||||||
index={index}
|
priority={challenge.priority}
|
||||||
showDescription={true}
|
status="todo"
|
||||||
maxTags={2}
|
dueDate={challenge.deadline}
|
||||||
|
todosCount={challenge.todosCount}
|
||||||
|
availableTags={availableTags as Tag[]}
|
||||||
|
source="manual"
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -295,118 +513,6 @@ export default function ManagerWeeklySummary({
|
|||||||
</div>
|
</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 */}
|
{/* Vue Métriques */}
|
||||||
{activeView === 'metrics' && <MetricsTab />}
|
{activeView === 'metrics' && <MetricsTab />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,58 +6,9 @@ import { StatCard } from '@/components/ui/StatCard';
|
|||||||
import { ActionCard } from '@/components/ui/ActionCard';
|
import { ActionCard } from '@/components/ui/ActionCard';
|
||||||
import { TaskCard } from '@/components/ui/TaskCard';
|
import { TaskCard } from '@/components/ui/TaskCard';
|
||||||
import { MetricCard } from '@/components/ui/MetricCard';
|
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 { SkeletonCard } from '@/components/ui/SkeletonCard';
|
||||||
import { AchievementData } from '@/components/ui/AchievementCard';
|
|
||||||
import { ChallengeData } from '@/components/ui/ChallengeCard';
|
|
||||||
|
|
||||||
export function CardsSection() {
|
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 (
|
return (
|
||||||
<section id="cards" className="space-y-8">
|
<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">
|
<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>
|
||||||
</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 */}
|
{/* Metric Cards */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-lg font-medium text-[var(--foreground)]">
|
<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 { Tabs } from './Tabs';
|
||||||
export { PriorityBadge } from './PriorityBadge';
|
export { PriorityBadge } from './PriorityBadge';
|
||||||
export { StatusBadge } from './StatusBadge';
|
export { StatusBadge } from './StatusBadge';
|
||||||
export { AchievementCard } from './AchievementCard';
|
|
||||||
export { ChallengeCard } from './ChallengeCard';
|
|
||||||
|
|
||||||
// Composants Daily
|
// Composants Daily
|
||||||
export { CheckboxItem } from './CheckboxItem';
|
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)
|
// 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
|
// 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({
|
const allRelatedTodos = await prisma.dailyCheckbox.count({
|
||||||
where: {
|
where: {
|
||||||
task: {
|
task: {
|
||||||
id: task.id,
|
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é
|
// 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 déjà comptés dans les tâches complétées
|
||||||
|
// Exclure les todos de type "meeting" (réunion)
|
||||||
const standaloneTodos = checkboxes.filter(
|
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) => {
|
standaloneTodos.forEach((todo) => {
|
||||||
// Appliquer la nouvelle règle de priorité :
|
// 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';
|
const impact: 'high' | 'medium' | 'low' = 'low';
|
||||||
|
|
||||||
accomplishments.push({
|
accomplishments.push({
|
||||||
id: `todo-${todo.id}`,
|
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
|
tags: [], // Todos standalone n'ont pas de tags par défaut
|
||||||
impact,
|
impact,
|
||||||
completedAt: todo.date,
|
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.length).toBeGreaterThan(0);
|
||||||
expect(realChanges.some((c) => c.includes('Projet'))).toBe(true);
|
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