Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
- package-ecosystem: "nuget"
directory: "/"
schedule:
interval: "weekly"

- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "weekly"

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
96 changes: 74 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: .NET CI/CD Pipeline
name: TradeOffStack CI/CD Pipeline

on:
push:
branches: [ "main" ]
branches: [ "main", "feature/postman-tests" ]
pull_request:
branches: [ "main" ]
branches: [ "main", "feature/postman-tests" ]

jobs:
build-and-test:
Expand All @@ -19,29 +19,81 @@ jobs:
with:
dotnet-version: '10.0.x'

- name: 📦 Restore dependencies
- name: 📦 Restore backend dependencies
run: dotnet restore TradeOffStackAPI.sln

- name: 🔨 Build API
run: dotnet build TradeOffStackAPI.sln --no-restore --configuration Release

- name: 🧪 Run Tests
- name: 🧪 Run C# Backend Tests
run: dotnet test TradeOffStackAPI.sln --no-build --configuration Release --verbosity normal

# Décommentez ce bloc (deploy) lorsque vous aurez votre serveur VPS configuré
# deploy:
# needs: build-and-test
# runs-on: ubuntu-latest
# if: github.ref == 'refs/heads/main'
# steps:
# - name: 🚀 Deploy to VPS via SSH
# uses: appleboy/ssh-action@master
# with:
# host: ${{ secrets.VPS_HOST }}
# username: ${{ secrets.VPS_USERNAME }}
# key: ${{ secrets.VPS_SSH_KEY }}
# script: |
# cd /var/www/TradeOffStackAPI
# git pull origin main
# docker-compose down
# docker-compose up -d --build
- name: 🚀 Start Services (API & Database) via Docker Compose
run: |
echo "POSTGRES_USER=tradeoff_admin" >> .env
echo "POSTGRES_PASSWORD=Tr@de0ff_Secure!2026_Db#X9" >> .env
echo "POSTGRES_DB=tradeoffstack" >> .env
echo "POSTGRES_PORT=5432" >> .env
echo "JWT_SECRET_KEY=Tr@de0ff_Super_Secret_Key_For_JWT_Auth_2026!" >> .env
docker compose up -d --build

- name: 🩺 Wait for API to be healthy
run: |
echo "Waiting for API to respond at http://localhost:5000/api/equipment..."
for i in {1..30}; do
if curl -s http://localhost:5000/api/equipment > /dev/null; then
echo "API is up and running!"
exit 0
fi
sleep 2
done
echo "API failed to start in time."
docker compose logs
exit 1

- name: 🟢 Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: frontend/package-lock.json

- name: 📦 Install Frontend Dependencies
run: |
cd frontend
npm ci

- name: 🎭 Install Playwright Browsers
run: |
cd frontend
npx playwright install --with-deps chromium

- name: ⚡ Start Frontend Vite Server
run: |
cd frontend
npm run dev &
echo "Waiting for Vite frontend to be responsive..."
for i in {1..15}; do
if curl -s http://localhost:5173 > /dev/null; then
echo "Vite is up!"
exit 0
fi
sleep 1
done

- name: 🧪 Run Playwright E2E UI Tests
run: |
cd frontend
node ui_test.js

- name: 🧪 Run Playwright E2E Global CRUD Lifecycle Tests
run: |
cd frontend
node ui_global_test.js

- name: 📤 Upload E2E Screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-screenshots
path: frontend/screenshots/
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,6 @@ coverage/
# --- Agents / AI ---
.claude/
.junie/

# --- Frontend (Separate Repo) ---
# frontend/
39 changes: 39 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# ==========================================
# 1. BUILD STAGE
# ==========================================
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

# Copier le fichier de solution et les projets (optimisation du cache Docker)
COPY ["TradeOffStackAPI.sln", "./"]
COPY ["TradeOffStackAPI/TradeOffStackAPI.csproj", "TradeOffStackAPI/"]
COPY ["TradeOffStackAPI.Tests/TradeOffStackAPI.Tests.csproj", "TradeOffStackAPI.Tests/"]

# Restaurer les dépendances
RUN dotnet restore "TradeOffStackAPI.sln"

# Copier tout le code source
COPY . .

# Compiler et publier l'API en Release
WORKDIR "/src/TradeOffStackAPI"
RUN dotnet publish "TradeOffStackAPI.csproj" -c Release -o /app/publish /p:UseAppHost=false

# ==========================================
# 2. RUNTIME STAGE (Production)
# ==========================================
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app

# Sécurité : utiliser l'utilisateur non-root 'app' intégré dans les images .NET 8+
USER app

# Copier les fichiers compilés depuis le build stage
COPY --from=build /app/publish .

# Définir le port d'écoute (8080 est le standard par défaut dans .NET 8+)
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080

# Lancer l'application
ENTRYPOINT ["dotnet", "TradeOffStackAPI.dll"]
8 changes: 7 additions & 1 deletion TradeOffStackAPI.Tests/Services/ReservationServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,13 @@ public async Task CreateReservationAsync_WhenEquipmentAvailable_ShouldSucceed()

_mockEquipmentRepo.Setup(r => r.GetByIdAsync(equipmentId)).ReturnsAsync(equipment);
_mockRepo.Setup(r => r.HasActiveReservationAsync(equipmentId)).ReturnsAsync(false);
_mockRepo.Setup(r => r.AddAsync(It.IsAny<Reservation>())).ReturnsAsync(true);
_mockRepo.Setup(r => r.AddAsync(It.IsAny<Reservation>()))
.Callback<Reservation>(r =>
{
_context.Reservations.Add(r);
_context.SaveChanges();
})
.ReturnsAsync(true);
_mockEquipmentRepo.Setup(r => r.UpdateAsync(It.IsAny<Equipment>())).ReturnsAsync(true);

var reservation = NewReservation(equipmentId);
Expand Down
4 changes: 1 addition & 3 deletions TradeOffStackAPI/Controllers/MaintenanceRequestController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@ public async Task<IActionResult> Cancel(Guid id)
[Authorize(Roles = Roles.AdminOrManager)]
public async Task<IActionResult> Delete(Guid id)
{
// Note: La logique métier de suppression n'est pas implémentée dans le service,
// car annuler est généralement préférable. On pourrait l'ajouter si nécessaire.
var response = await _service.CancelRequestAsync(id);
var response = await _service.DeleteRequestAsync(id);
return response.Success ? NoContent() : NotFound(new { message = response.Message });
}
}
Expand Down
7 changes: 7 additions & 0 deletions TradeOffStackAPI/Controllers/ReservationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,11 @@ public async Task<IActionResult> Cancel(Guid id)
var response = await _service.CancelReservationAsync(id);
return response.Success ? NoContent() : NotFound(new { message = response.Message });
}

[HttpDelete("{id}")]
public async Task<IActionResult> Delete(Guid id)
{
var response = await _service.DeleteReservationAsync(id);
return response.Success ? NoContent() : NotFound(new { message = response.Message });
}
}
22 changes: 22 additions & 0 deletions TradeOffStackAPI/Extensions/ProgramExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,28 @@ public static async Task ApplyDatabaseMigrationsAsync(this WebApplication app)
{
logger.LogInformation("SQLite provider detected — migration skipped (EnsureCreated is used).");
}

// ==========================================
// SÉCURITÉ : Initialisation de l'Administrateur par défaut
// ==========================================
if (!await db.Users.AnyAsync())
{
logger.LogInformation("No users found. Creating default Administrator account...");
var adminUser = new TradeOffStackAPI.Models.User
{
Id = Guid.NewGuid(),
FirstName = "System",
LastName = "Admin",
Email = "admin@tradeoffstack.com",
Role = TradeOffStackAPI.Models.UserRole.Admin,
IsActive = true,
PasswordHash = BCrypt.Net.BCrypt.HashPassword("Admin123!Secure")
};

await db.Users.AddAsync(adminUser);
await db.SaveChangesAsync();
logger.LogInformation("Default Administrator account created (admin@tradeoffstack.com / Admin123!Secure).");
}
}
catch (Exception ex)
{
Expand Down
11 changes: 11 additions & 0 deletions TradeOffStackAPI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

// --- Composition de la DI ---
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});

builder.Services
.AddPersistence(builder.Configuration)
.AddApplicationServices(builder.Configuration)
Expand All @@ -29,6 +39,7 @@
// --- Pipeline HTTP ---
app.UseForwardedHeaders();
app.UseExceptionHandling();
app.UseCors("AllowAll");

if (app.Environment.IsDevelopment())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ public interface IMaintenanceRequestService
Task<ServiceResponse<MaintenanceRequest>> UpdateRequestAsync(Guid id, MaintenanceRequest request);
Task<ServiceResponse> CompleteRequestAsync(Guid id, string? technicianNotes);
Task<ServiceResponse> CancelRequestAsync(Guid id);
Task<ServiceResponse> DeleteRequestAsync(Guid id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ public interface IReservationService
Task<ServiceResponse<Reservation>> UpdateReservationAsync(Guid id, Reservation reservation);
Task<ServiceResponse> ReturnEquipmentAsync(Guid reservationId);
Task<ServiceResponse> CancelReservationAsync(Guid id);
Task<ServiceResponse> DeleteReservationAsync(Guid id);
}
Loading
Loading