feat: добавил поддержку подписки на календарь и экспорт расписания лекций в формате .ics
Backend CI / build-and-test (push) Successful in 57s
Frontend CI / build-and-check (push) Failing after 26s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 11s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 2m33s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 33s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 8s

This commit is contained in:
2026-06-02 21:26:48 +03:00
parent 7050851bd4
commit 136bcce7db
16 changed files with 639 additions and 8 deletions
@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using UniVerse.Application.DTOs.Notifications;
@@ -193,6 +194,56 @@ public class UserServiceTests
Assert.Equal(2, Assert.Single(result.Items).Id);
}
[Fact]
public async Task CalendarSubscriptionToken_Roundtrip_ReturnsUserEnrollmentsIcs()
{
await using var db = CreateDbContext();
var startsAt = new DateTime(2026, 1, 10, 9, 0, 0, DateTimeKind.Utc);
db.Users.Add(new User { Id = 1, Email = "student@test.local" });
db.Courses.Add(new Course { Id = 1, Name = "Course" });
db.Lectures.Add(Lecture(1, startsAt));
db.LectureEnrollments.Add(new LectureEnrollment { LectureId = 1, UserId = 1 });
await db.SaveChangesAsync();
var service = CreateService(db);
var token = await service.GetCalendarSubscriptionTokenAsync(1);
var ics = await service.GetEnrollmentsIcsBySubscriptionTokenAsync(token);
Assert.Contains("BEGIN:VCALENDAR", ics);
Assert.Contains("Lecture 1", ics);
}
[Fact]
public async Task CalendarSubscriptionToken_RejectsTamperedToken()
{
await using var db = CreateDbContext();
db.Users.Add(new User { Id = 1, Email = "student@test.local" });
await db.SaveChangesAsync();
var service = CreateService(db);
var token = await service.GetCalendarSubscriptionTokenAsync(1);
var tampered = token[..^1] + (token[^1] == 'A' ? 'B' : 'A');
await Assert.ThrowsAsync<ForbiddenException>(() =>
service.GetEnrollmentsIcsBySubscriptionTokenAsync(tampered));
}
[Fact]
public async Task GetEnrollmentIcsAsync_ReturnsLectureIcsWithoutEnrollment()
{
await using var db = CreateDbContext();
var startsAt = new DateTime(2026, 2, 10, 9, 0, 0, DateTimeKind.Utc);
db.Users.Add(new User { Id = 1, Email = "student@test.local" });
db.Courses.Add(new Course { Id = 1, Name = "Course" });
db.Lectures.Add(Lecture(1853, startsAt));
await db.SaveChangesAsync();
var service = CreateService(db);
var ics = await service.GetEnrollmentIcsAsync(1, 1853);
Assert.Contains("BEGIN:VCALENDAR", ics);
Assert.Contains("Lecture 1853", ics);
}
private static AppDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
@@ -215,7 +266,13 @@ public class UserServiceTests
.Returns(Task.CompletedTask);
var gamification = new GamificationService(db, notifications, NullLogger<GamificationService>.Instance);
return new UserService(db, gamification);
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Jwt:Secret"] = "test-calendar-subscription-secret-32chars"
})
.Build();
return new UserService(db, gamification, config);
}
private static void SeedLevelThresholds(AppDbContext db)