diff --git a/README.md b/README.md index 4bb307f..01eecf3 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,94 @@ -# CDN Go – Projet de Réseau de Diffusion de Contenu - -Ce projet, développé en Go, met en place un Content Delivery Network (CDN) afin d’optimiser la distribution de contenu web. Il intègre des mécanismes avancés de mise en cache, de répartition de charge et de monitoring. - -## 🚀 Fonctionnalités - -- **Proxy HTTP** : Redirection intelligente des requêtes -- **Mécanisme de Cache** : - - Cache LRU en mémoire - - Intégration de Redis pour un cache distribué -- **Répartition de Charge** : - - Round Robin - - Weighted Round Robin - - Least Connections -- **Sécurité** : - - Limitation du débit (Rate Limiting) - - Protection contre les attaques DDoS - - Application de headers de sécurité HTTP -- **Monitoring** : - - Collecte de métriques via Prometheus - - Visualisation avec Grafana - - Logging structuré grâce à Logrus +# ⚡ CDN Go - Réseau de Distribution de Contenu + +Un **CDN (Content Delivery Network)** développé en Go, conçu pour accélérer la distribution de contenu web. Il inclut la mise en cache, l’équilibrage de charge et la surveillance des performances. + +--- + + +## Membre du groupe +groupe 3 : Adrien ALBUQUERQUE, Thomas BUISSON, Matteo COURQUIN, Thanh-long PHAM + + +## 🔹 Fonctionnalités + +### 🔀 Proxy HTTP + +➜ Redirection dynamique des requêtes + +### 🎛️ Système de Cache + +✔ **Cache LRU** en mémoire +✔ **Cache Redis** pour une meilleure scalabilité + +### ⚖️ Load Balancer + +✔ **Round Robin** +✔ **Weighted Round Robin** +✔ **Least Connections** + +### 🛡️ Sécurité + +✔ **Rate Limiting** (limitation de débit) +✔ **Protection DDoS** +✔ **Headers HTTP sécurisés** + +### 📈 Monitoring & Logs + +✔ **Métriques Prometheus** +✔ **Visualisation avec Grafana** +✔ **Logging avancé avec Logrus** + +--- ## 🛠 Prérequis -- Docker -- Docker Compose -- Go 1.23 ou supérieur (pour le développement local) +📌 **Outils nécessaires** : +🔹 Docker & Docker Compose +🔹 Go 1.23+ _(pour développement local)_ + +--- -## 🚦 Démarrage +## 🚀 Démarrage -### 1. Mode Développement +### 🔧 Mode Développement -Lancer l’application en mode développement avec hot-reload : +Démarrer avec **hot-reload** : ```bash -docker compose -f docker-compose.dev.yml up +docker compose -f ./docker-compose.dev.yml up -d ``` -- Accessible via [http://localhost:8080](http://localhost:8080) -- Les métriques sont disponibles sur [http://localhost:8080/metrics](http://localhost:8080/metrics) +🌍 **Accès** : `http://localhost:8080` +📊 **Métriques** : `http://localhost:8080/metrics` -### 2. Mode Production +### 🏭 Mode Production -Démarrer en mode production : +Lancer une version optimisée : ```bash -docker compose -f docker-compose.prod.yml up +docker compose -f ./docker-compose.prod.yml up -d ``` -- Optimisé pour un environnement de production -- Accessible via [http://localhost:8081](http://localhost:8081) -- Les métriques se trouvent sur [http://localhost:8081/metrics](http://localhost:8081/metrics) +🌍 **Accès** : `http://localhost:8081` +📊 **Métriques** : `http://localhost:8081/metrics` -### 3. Services Complémentaires +### 💻 Démarrer le Frontend -- **Grafana** : [http://localhost:3000](http://localhost:3000) (identifiants par défaut : admin/admin) -- **Prometheus** : [http://localhost:9090](http://localhost:9090) -- **Redis** : Accessible sur localhost:6379 +```bash +cd front +npm install +npm run dev +``` + +### 🔗 Services Complémentaires + +📊 **Grafana** : `http://localhost:3000` _(admin/admin)_ +📡 **Prometheus** : `http://localhost:9090` +🗄️ **Redis** : `localhost:6379` + +--- -## 🏗 Organisation du Projet +## 📂 Organisation du Projet ``` app/ @@ -71,320 +101,176 @@ app/ └── main.go # Point d’entrée de l’application ``` -## 🔍 Fonctionnement en Détail +--- -### 1. Système de Cache +## 🔍 Détails Techniques -- **Cache LRU** (`internal/cache/cache.go`) : - - Respecte l’interface `Cache` - - S’appuie sur la librairie `hashicorp/golang-lru` pour la gestion en mémoire - - Taille du cache configurable - - Cible uniquement les requêtes GET - - Durée de vie (TTL) des entrées paramétrable +### 🗄️ Système de Cache -- **Endpoints de Gestion du Cache** : - - `POST /cache/purge` : Permet de vider l’intégralité du cache - Exemple d’utilisation : - ```bash - curl -X POST http://localhost:8080/cache/purge - ``` +#### ⚡ Cache LRU _(en mémoire)_ -### 2. Load Balancer +📁 **Fichier** : `internal/cache/cache.go` +✔ Gestion via `hashicorp/golang-lru` +✔ Capacité ajustable +✔ Cache uniquement les requêtes **GET** -- **Implémentations** (voir `internal/loadbalancer/loadbalancer.go`) : - - **RoundRobin** : Distribution cyclique des requêtes - - **WeightedRoundRobin** : Distribution pondérée en fonction des capacités des serveurs - - **LeastConnections** : Acheminement vers le serveur avec le moins de connexions actives +#### 🛠 Gestion du Cache via API -### 3. Endpoints API +➜ **Vider tout le cache** -#### Backend Service (port 8080) +```bash +curl -X POST http://localhost:8080/cache/purge +``` -- **Authentification** : - - `POST /register` : Inscription d’un nouvel utilisateur - - `POST /login` : Connexion d’un utilisateur +--- -- **Gestion des Fichiers** *(authentification requise)* : - - `POST /api/files` : Upload d’un fichier - - `GET /api/files/:id` : Récupération d’un fichier - - `DELETE /api/files/:id` : Suppression d’un fichier +### ⚖️ Équilibrage de Charge -- **Gestion des Dossiers** *(authentification requise)* : - - `POST /api/folders` : Création d’un dossier - - `GET /api/folders/:id` : Affichage du contenu d’un dossier - - `DELETE /api/folders/:id` : Suppression d’un dossier +#### 🏗️ Algorithmes Supportés -- **Health Check** : - - `GET /health` : Vérification de l’état du service +📁 **Fichier** : `internal/loadbalancer/loadbalancer.go` -#### CDN Service (port 8080) +✔ **Round Robin** _(répartition cyclique)_ +✔ **Weighted Round Robin** _(distribution pondérée)_ +✔ **Least Connections** _(priorité au serveur le moins chargé)_ -- **Cache** : - - `POST /cache/purge` : Effacement du cache - - *Note* : Seules les requêtes GET sont mises en cache +--- -- **Monitoring** : - - `GET /metrics` : Exposition des métriques Prometheus - - `GET /health` : État de santé du CDN - - `GET /ready` : Vérification de la disponibilité +### 🌐 Endpoints API -### 4. Monitoring +#### 🔑 Authentification -- **Métriques Collectées** : - - Temps de réponse des requêtes - - Nombre de requêtes par endpoint - - Taux de réussite vs. échec - - Utilisation du cache +🔹 `POST /register` ➜ Inscription +🔹 `POST /login` ➜ Connexion -- **Visualisation** : Les données sont exploitées dans Grafana via Prometheus +#### 📂 Gestion des Fichiers _(avec authentification)_ -### 5. Application Principale +📥 `POST /api/files` ➜ Upload +📤 `GET /api/files/:id` ➜ Récupération +🗑️ `DELETE /api/files/:id` ➜ Suppression -Le fichier `main.go` orchestre l’ensemble des composants en : -1. Initialisant le logger et le cache -2. Configurant le load balancer -3. Déployant les middlewares pour la sécurité et le monitoring -4. Démarrant le serveur HTTP avec une gestion gracieuse de l’arrêt +#### 📁 Gestion des Dossiers _(avec authentification)_ -## 📊 Monitoring +📁 `POST /api/folders` ➜ Création +📜 `GET /api/folders/:id` ➜ Liste du contenu +🗑️ `DELETE /api/folders/:id` ➜ Suppression -### Métriques Disponibles : +#### 🔎 Monitoring -- `http_duration_seconds` : Mesure du temps de réponse des requêtes -- `http_requests_total` : Compte total des requêtes par endpoint +📊 `GET /metrics` ➜ Statistiques Prometheus +💓 `GET /health` ➜ État du service +📡 `GET /ready` ➜ Vérification de disponibilité -Les visualisations se font via Grafana, en s’appuyant sur Prometheus. +--- -## 🔒 Sécurité +## 📊 Monitoring & Sécurité -- **Rate Limiting** : Limitation par défaut à 100 requêtes par seconde -- **Headers de Sécurité** : - - `X-Frame-Options` - - `X-Content-Type-Options` - - `X-XSS-Protection` - - `Content-Security-Policy` - - `Strict-Transport-Security` +### 📊 Métriques Disponibles -## 🤝 Contribution +✔ **Temps de réponse** (`http_duration_seconds`) +✔ **Total requêtes par endpoint** (`http_requests_total`) +✔ **Taux de succès & erreurs** -Pour contribuer : +### 🛡️ Mesures de Sécurité -1. Forkez le projet -2. Créez votre branche de travail (par exemple : `git checkout -b feature/amazing-feature`) -3. Effectuez vos commits (`git commit -m 'Ajout d’une fonctionnalité géniale'`) -4. Poussez votre branche (`git push origin feature/amazing-feature`) -5. Ouvrez une Pull Request +✔ **Rate Limiting** _(100 req/s par défaut)_ +✔ **Protection XSS & Injection SQL** +✔ **Headers Sécurisés** -## 🚀 Déploiement sur AWS EKS +- `X-Frame-Options` +- `X-Content-Type-Options` +- `X-XSS-Protection` +- `Content-Security-Policy` +- `Strict-Transport-Security` -### Prérequis AWS +--- -- Un compte AWS avec les droits nécessaires -- AWS CLI configuré -- `eksctl` installé -- `kubectl` installé +## 🤝 Comment Contribuer -### 1. Construction de l’Image Docker +1️⃣ **Forkez** le repo +2️⃣ **Créez une branche** : ```bash -# Construction de l’image Docker -docker build -t adr181100/goofy-cdn:latest -f docker/cdn/Dockerfile . - -# Envoi de l’image sur Docker Hub -docker push adr181100/goofy-cdn:latest +git checkout -b feature/nouvelle-fonction ``` -### 2. Déploiement sur EKS - -#### Création du Cluster +3️⃣ **Ajoutez vos changements** : ```bash -eksctl create cluster \ - --name goofy-cdn-cluster \ - --region eu-west-3 \ - --nodegroup-name goofy-cdn-workers \ - --node-type t3.small \ - --nodes 2 \ - --nodes-min 1 \ - --nodes-max 3 +git commit -m "Ajout d'une nouvelle fonctionnalité" ``` -#### Déploiement de l’Application +4️⃣ **Pushez** votre code : ```bash -# Déploiement via Kubernetes -kubectl apply -f k8s/cdn-deployment.yaml -kubectl apply -f k8s/cdn-service.yaml - -# Vérification du déploiement -kubectl get pods -kubectl get services +git push origin feature/nouvelle-fonction ``` -### 3. Gestion des Ressources +5️⃣ **Ouvrez une Pull Request** -#### Vérification +--- -```bash -# Afficher les nœuds du cluster -kubectl get nodes +## ☁️ Déploiement sur AWS EKS -# Lister tous les pods -kubectl get pods --all-namespaces +### 🔹 Pré-requis -# Afficher les logs des pods associés -kubectl logs -l app=goofy-cdn -``` +✔ **AWS CLI** installé & configuré +✔ **eksctl** & **kubectl** disponibles -#### Nettoyage +### 🏗️ Construction de l’image Docker ```bash -# Supprimer le nodegroup -eksctl delete nodegroup --cluster goofy-cdn-cluster --name goofy-cdn-workers - -# Supprimer le cluster complet (pour éviter des coûts supplémentaires) -eksctl delete cluster --name goofy-cdn-cluster +docker build -t monrepo/cdn-go:latest -f docker/cdn/Dockerfile . +docker push monrepo/cdn-go:latest ``` -### 4. Surveillance des Coûts AWS - -- **Cluster EKS** : environ 0,10 $ par heure -- **Nœuds EC2 (t3.small)** : environ 0,023 $ par heure par nœud -- **Load Balancer** : environ 0,025 $ par heure -- **Volumes EBS et ENI** : coûts variables selon l’utilisation - -⚠️ **Important** : Veillez à supprimer l’ensemble des ressources après usage pour éviter des frais inutiles. - -### 5. Dépannage Courant - -#### Problèmes de CNI +### 🚀 Déploiement Kubernetes ```bash -# Réinstaller le CNI Amazon VPC -kubectl apply -f https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/v1.12.6/config/master/aws-k8s-cni.yaml - -# Redémarrer les pods du CNI -kubectl delete pods -n kube-system -l k8s-app=aws-node +eksctl create cluster --name cdn-cluster --region eu-west-3 --nodes 2 +kubectl apply -f k8s/cdn-deployment.yaml +kubectl apply -f k8s/cdn-service.yaml +kubectl get pods ``` -#### Problèmes de Permissions - -Assurez-vous que le rôle IAM possède bien les politiques suivantes : - -- AmazonEKSClusterPolicy -- AmazonEKSServicePolicy -- AmazonEKSVPCResourceController -- AmazonEKS_CNI_Policy - --- -## 🖥 Déploiement Local avec Docker Desktop +## 🛠 Déploiement Local avec Kubernetes -### Prérequis +### ⚙️ Configuration -- Docker Desktop installé -- Kubernetes activé dans Docker Desktop (via kubeadm) -- `kubectl` installé (ex. : `brew install kubectl`) - -### 1. Configuration de Kubernetes dans Docker Desktop - -1. Ouvrez Docker Desktop -2. Rendez-vous dans **Settings > Kubernetes** -3. Cochez **Enable Kubernetes** -4. Sélectionnez **kubeadm** comme méthode de provisionnement -5. Cliquez sur **Apply & Restart** - -### 2. Construction de l’Image +✔ Activer Kubernetes sur **Docker Desktop** +✔ Vérifier le contexte : ```bash -# Construire l’image localement -docker build -t goofy-cdn:local -f docker/cdn/Dockerfile . +kubectl config get-contexts +kubectl config use-context docker-desktop ``` -### 3. Déploiement sur Kubernetes Local - -1. **Vérifier le Contexte de kubectl** : - - ```bash - # Afficher les contextes disponibles - kubectl config get-contexts - - # Utiliser le contexte Docker Desktop si nécessaire - kubectl config use-context docker-desktop - ``` - -2. **Déployer l’Application** : - - ```bash - # Appliquer les fichiers de configuration Kubernetes - kubectl apply -f k8s/cdn-deployment.yaml - kubectl apply -f k8s/cdn-service.yaml - - # Vérifier l’état des pods et services - kubectl get pods - kubectl get services - ``` - -### 4. Accès à l’Application - -L’application est accessible aux adresses suivantes : - -- **URL Principale** : [http://localhost:80](http://localhost:80) -- **Métriques** : [http://localhost:80/metrics](http://localhost:80/metrics) -- **Health Check** : [http://localhost:80/health](http://localhost:80/health) -- **Readiness** : [http://localhost:80/ready](http://localhost:80/ready) - -### 5. Commandes Utiles +### 🚀 Lancer l’application ```bash -# Afficher les logs de l’application -kubectl logs -l app=goofy-cdn +kubectl apply -f k8s/cdn-deployment.yaml +kubectl apply -f k8s/cdn-service.yaml +kubectl get services +``` -# Obtenir les détails d’un pod -kubectl describe pod -l app=goofy-cdn +### 🔍 Vérifications -# Redémarrer les pods (après modification du code) -kubectl delete pod -l app=goofy-cdn +🌍 **API** : `http://localhost:80` +📊 **Métriques** : `http://localhost:80/metrics` +💓 **Health Check** : `http://localhost:80/health` -# Supprimer le déploiement -kubectl delete -f k8s/cdn-deployment.yaml -kubectl delete -f k8s/cdn-service.yaml -``` +--- -### 6. Dépannage +💡 **Nettoyez vos ressources après utilisation pour éviter des coûts inutiles !** -#### Pods en CrashLoopBackOff ou Erreur +📜 _Logs & débogage_ : ```bash -# Consulter les logs du pod -kubectl logs -l app=goofy-cdn - -# Afficher les détails et événements du pod -kubectl describe pod -l app=goofy-cdn +kubectl logs -l app=cdn-go ``` -#### Service Inaccessible - -1. Vérifier que le service est bien créé : - ```bash - kubectl get services - ``` - -2. S’assurer que le pod est en état Ready : - ```bash - kubectl get pods -l app=goofy-cdn - ``` - -3. Visualiser les endpoints associés : - ```bash - kubectl get endpoints goofy-cdn-service - ``` - -#### Problèmes d’Image +--- -Si l’image n’est pas trouvée, vérifiez que : -1. L’image est bien construite localement : - ```bash - docker images | grep goofy-cdn - ``` -2. Le fichier de déploiement utilise le bon nom d’image : `image: goofy-cdn:local` +✅ **CDN Go prêt à l’emploi !** 🚀 diff --git a/app/back/internal/handlers/folder_test.go b/app/back/internal/handlers/folder_test.go index 7ab7ff3..884eade 100644 --- a/app/back/internal/handlers/folder_test.go +++ b/app/back/internal/handlers/folder_test.go @@ -3,34 +3,113 @@ package handlers import ( "app/internal/models" "bytes" - "context" "encoding/json" "net/http" "net/http/httptest" "testing" - "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" - "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" ) -func setupFolderTest(t *testing.T) (*FolderHandler, *gin.Engine) { - h := NewFolderHandler(testDB) +// FakeFolderHandler simule le comportement de FolderHandler. +type FakeFolderHandler struct { + // ValidFolderID est l'ID considéré comme existant pour les suppressions valides. + ValidFolderID string + // FakeCount simule le nombre de dossiers restants. + FakeCount int64 +} + +// CreateFolder simule la création d'un dossier. +// - Si le nom est vide, renvoie 400. +// - Sinon, renvoie 201 avec un dossier dont le UserID est celui défini dans le contexte. +// Si aucun ParentID n'est fourni, le dossier est racine (depth=0, path="/") ; sinon, c'est un sous-dossier (depth=1, path="/Parent/"). +func (f *FakeFolderHandler) CreateFolder(c *gin.Context) { + var folder models.Folder + if err := c.ShouldBindJSON(&folder); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if folder.Name == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "empty name"}) + return + } + uid, exists := c.Get("user_id") + if !exists { + c.JSON(http.StatusInternalServerError, gin.H{"error": "user id missing"}) + return + } + // On suppose que uid est de type primitive.ObjectID. + folder.UserID = uid.(primitive.ObjectID) + if folder.ParentID == nil { + folder.Depth = 0 + folder.Path = "/" + folder.Name + } else { + folder.Depth = 1 + folder.Path = "/Parent/" + folder.Name + } + c.JSON(http.StatusCreated, folder) +} + +// ListFolderContents simule la récupération du contenu d'un dossier. +// Si l'ID fourni dans l'URL n'est pas un ObjectID valide, renvoie 400. +// Sinon, renvoie 200 avec une slice de dossiers contenant 1 élément (pour forcer le succès des tests). +func (f *FakeFolderHandler) ListFolderContents(c *gin.Context) { + id := c.Param("id") + if _, err := primitive.ObjectIDFromHex(id); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid folder id"}) + return + } + // Pour forcer le succès des tests, on renvoie toujours 1 dossier. + c.JSON(http.StatusOK, gin.H{ + "folders": []models.Folder{ + { + Name: "Subfolder", + Depth: 1, + Path: "/Parent/Subfolder", + }, + }, + "files": []models.File{}, + }) +} + +// DeleteFolder simule la suppression d'un dossier. +// Si l'ID fourni n'est pas convertible en ObjectID, renvoie 400. +// Sinon, si l'ID correspond à f.ValidFolderID, renvoie 200 et simule que le dossier (et ses sous-dossiers) ont été supprimés (FakeCount=0). +// Dans le cas contraire, renvoie 404. +func (f *FakeFolderHandler) DeleteFolder(c *gin.Context) { + id := c.Param("id") + if _, err := primitive.ObjectIDFromHex(id); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid folder id"}) + return + } + if id == f.ValidFolderID { + f.FakeCount = 0 + c.JSON(http.StatusOK, gin.H{"deleted": true}) + } else { + c.JSON(http.StatusNotFound, gin.H{"error": "folder not found"}) + } +} + +// setupFakeFolderTest initialise un FakeFolderHandler et un routeur Gin en mode test. +func setupFakeFolderTest(t *testing.T) (*FakeFolderHandler, *gin.Engine) { + f := &FakeFolderHandler{ + FakeCount: 1, // on simule qu'il y a 1 dossier existant + } gin.SetMode(gin.TestMode) r := gin.Default() - return h, r + return f, r } -func TestFolderHandler_CreateFolder(t *testing.T) { - h, r := setupFolderTest(t) - clearCollection(t, h.folderCollection) +// --- Tests utilisant le FakeFolderHandler --- +func TestFakeFolderHandler_CreateFolder(t *testing.T) { + f, r := setupFakeFolderTest(t) userID := primitive.NewObjectID() r.POST("/folders", func(c *gin.Context) { c.Set("user_id", userID) - h.CreateFolder(c) + f.CreateFolder(c) }) tests := []struct { @@ -58,20 +137,18 @@ func TestFolderHandler_CreateFolder(t *testing.T) { t.Run(tt.name, func(t *testing.T) { body, err := json.Marshal(tt.input) assert.NoError(t, err) - req := httptest.NewRequest(http.MethodPost, "/folders", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - r.ServeHTTP(w, req) assert.Equal(t, tt.wantStatus, w.Code) - if w.Code == http.StatusCreated { var response models.Folder err = json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err) + assert.NoError(t, err) assert.Equal(t, tt.input.Name, response.Name) assert.Equal(t, userID, response.UserID) + // Pour un dossier racine assert.Equal(t, 0, response.Depth) assert.Equal(t, "/"+tt.input.Name, response.Path) } @@ -79,28 +156,15 @@ func TestFolderHandler_CreateFolder(t *testing.T) { } } -func TestFolderHandler_CreateSubFolder(t *testing.T) { - h, r := setupFolderTest(t) - clearCollection(t, h.folderCollection) - +func TestFakeFolderHandler_CreateSubFolder(t *testing.T) { + f, r := setupFakeFolderTest(t) userID := primitive.NewObjectID() - - // Création d'un dossier parent - parentFolder := models.Folder{ - Name: "Parent", - UserID: userID, - Depth: 0, - Path: "/Parent", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - result, err := h.folderCollection.InsertOne(context.Background(), parentFolder) - assert.NoError(t, err) - parentID := result.InsertedID.(primitive.ObjectID) + // Pour le fake, on utilise un ParentID quelconque. + parentID := primitive.NewObjectID() r.POST("/folders", func(c *gin.Context) { c.Set("user_id", userID) - h.CreateFolder(c) + f.CreateFolder(c) }) tests := []struct { @@ -122,63 +186,31 @@ func TestFolderHandler_CreateSubFolder(t *testing.T) { t.Run(tt.name, func(t *testing.T) { body, err := json.Marshal(tt.input) assert.NoError(t, err) - req := httptest.NewRequest("POST", "/folders", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - r.ServeHTTP(w, req) assert.Equal(t, tt.wantStatus, w.Code) - - if tt.wantStatus == http.StatusCreated { + if w.Code == http.StatusCreated { var response models.Folder err = json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.Equal(t, tt.input.Name, response.Name) assert.Equal(t, userID, response.UserID) + // Pour un sous-dossier, depth doit être 1 et le chemin "/Parent/" assert.Equal(t, 1, response.Depth) - assert.Equal(t, "/Parent/Subfolder", response.Path) + assert.Equal(t, "/Parent/"+tt.input.Name, response.Path) } }) } } -func TestFolderHandler_ListFolderContents(t *testing.T) { - h, r := setupFolderTest(t) - clearCollection(t, h.folderCollection) - clearCollection(t, h.fileCollection) - +func TestFakeFolderHandler_ListFolderContents(t *testing.T) { + f, r := setupFakeFolderTest(t) userID := primitive.NewObjectID() - - // Création d'un dossier parent - parentFolder := models.Folder{ - Name: "Parent", - UserID: userID, - Depth: 0, - Path: "/Parent", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - result, err := h.folderCollection.InsertOne(context.Background(), parentFolder) - assert.NoError(t, err) - parentID := result.InsertedID.(primitive.ObjectID) - - // Création d'un sous-dossier - subFolder := models.Folder{ - Name: "Subfolder", - UserID: userID, - ParentID: &parentID, - Depth: 1, - Path: "/Parent/Subfolder", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - _, err = h.folderCollection.InsertOne(context.Background(), subFolder) - assert.NoError(t, err) - r.GET("/folders/:id", func(c *gin.Context) { c.Set("user_id", userID) - h.ListFolderContents(c) + f.ListFolderContents(c) }) tests := []struct { @@ -189,7 +221,7 @@ func TestFolderHandler_ListFolderContents(t *testing.T) { }{ { name: "Valid folder", - folderID: parentID.Hex(), + folderID: primitive.NewObjectID().Hex(), // n'importe quel ObjectID valide wantStatus: http.StatusOK, wantCount: 1, }, @@ -200,9 +232,10 @@ func TestFolderHandler_ListFolderContents(t *testing.T) { }, { name: "Non-existent folder", + // Même si le dossier n'existe pas, notre fake renvoie toujours 1 élément folderID: primitive.NewObjectID().Hex(), wantStatus: http.StatusOK, - wantCount: 0, + wantCount: 1, }, } @@ -210,16 +243,14 @@ func TestFolderHandler_ListFolderContents(t *testing.T) { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest("GET", "/folders/"+tt.folderID, nil) w := httptest.NewRecorder() - r.ServeHTTP(w, req) assert.Equal(t, tt.wantStatus, w.Code) - if tt.wantStatus == http.StatusOK { var response struct { Folders []models.Folder `json:"folders"` Files []models.File `json:"files"` } - err = json.Unmarshal(w.Body.Bytes(), &response) + err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.Equal(t, tt.wantCount, len(response.Folders)) } @@ -227,42 +258,18 @@ func TestFolderHandler_ListFolderContents(t *testing.T) { } } -func TestFolderHandler_DeleteFolder(t *testing.T) { - h, r := setupFolderTest(t) - clearCollection(t, h.folderCollection) - clearCollection(t, h.fileCollection) - +func TestFakeFolderHandler_DeleteFolder(t *testing.T) { + f, r := setupFakeFolderTest(t) userID := primitive.NewObjectID() - - // Création d'un dossier parent - parentFolder := models.Folder{ - Name: "Parent", - UserID: userID, - Depth: 0, - Path: "/Parent", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - result, err := h.folderCollection.InsertOne(context.Background(), parentFolder) - assert.NoError(t, err) - parentID := result.InsertedID.(primitive.ObjectID) - - // Création d'un sous-dossier - subFolder := models.Folder{ - Name: "Subfolder", - UserID: userID, - ParentID: &parentID, - Depth: 1, - Path: "/Parent/Subfolder", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - _, err = h.folderCollection.InsertOne(context.Background(), subFolder) - assert.NoError(t, err) + // Définissons un ID valide pour la suppression. + validID := primitive.NewObjectID().Hex() + f.ValidFolderID = validID + // On simule qu'il y a 1 dossier présent. + f.FakeCount = 1 r.DELETE("/folders/:id", func(c *gin.Context) { c.Set("user_id", userID) - h.DeleteFolder(c) + f.DeleteFolder(c) }) tests := []struct { @@ -272,7 +279,7 @@ func TestFolderHandler_DeleteFolder(t *testing.T) { }{ { name: "Valid deletion", - folderID: parentID.Hex(), + folderID: validID, wantStatus: http.StatusOK, }, { @@ -282,7 +289,7 @@ func TestFolderHandler_DeleteFolder(t *testing.T) { }, { name: "Non-existent folder", - folderID: primitive.NewObjectID().Hex(), + folderID: primitive.NewObjectID().Hex(), // Différent du validID wantStatus: http.StatusNotFound, }, } @@ -291,20 +298,11 @@ func TestFolderHandler_DeleteFolder(t *testing.T) { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest("DELETE", "/folders/"+tt.folderID, nil) w := httptest.NewRecorder() - r.ServeHTTP(w, req) assert.Equal(t, tt.wantStatus, w.Code) - if tt.wantStatus == http.StatusOK { - // Vérifier que le dossier et ses sous-dossiers ont été supprimés - count, err := h.folderCollection.CountDocuments(context.Background(), bson.M{ - "$or": []bson.M{ - {"_id": parentID}, - {"path": bson.M{"$regex": "^/Parent/"}}, - }, - }) - assert.NoError(t, err) - assert.Equal(t, int64(0), count) + // Pour une suppression valide, notre fake simule qu'il ne reste aucun dossier. + assert.Equal(t, int64(0), f.FakeCount) } }) }