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
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:
@@ -47,6 +47,9 @@ public class EndpointAuthorizationTests : IClassFixture<ApiWebApplicationFactory
|
||||
body: """{"displayName":"Test","avatarUrl":null}""");
|
||||
yield return E("users/me/stats [AnyAuth]", "GET", "api/v1/users/me/stats", "Student");
|
||||
yield return E("users/me/enrollments [AnyAuth]", "GET", "api/v1/users/me/enrollments", "Student");
|
||||
yield return E("users/me/enrollments/calendar-subscription [AnyAuth]", "GET", "api/v1/users/me/enrollments/calendar-subscription", "Student");
|
||||
yield return E("users/me/enrollments.ics [AnyAuth]", "GET", "api/v1/users/me/enrollments.ics", "Student");
|
||||
yield return E("users/me/enrollments/{id}.ics [AnyAuth]", "GET", "api/v1/users/me/enrollments/1.ics", "Student");
|
||||
yield return E("users/me/reviews [AnyAuth]", "GET", "api/v1/users/me/reviews", "Student");
|
||||
yield return E("users/me/achievements [AnyAuth]", "GET", "api/v1/users/me/achievements", "Student");
|
||||
yield return E("users/me/transactions [AnyAuth]", "GET", "api/v1/users/me/transactions", "Student");
|
||||
@@ -192,6 +195,7 @@ public class EndpointAuthorizationTests : IClassFixture<ApiWebApplicationFactory
|
||||
// dev login доступен в окружении Development
|
||||
yield return new object[] { "auth/login/dev POST", "POST", "api/v1/auth/login/dev",
|
||||
"""{"email":"test@test.com","displayName":"Test","role":"Student"}""" };
|
||||
yield return new object[] { "users/calendar/enrollments/{token}.ics GET", "GET", "api/v1/users/calendar/enrollments/bad-token.ics" };
|
||||
// refresh читает из cookie — возвращает 401, если нет cookie, но это не 401 от промежуточного ПО авторизации
|
||||
// (он возвращает 401 явно в теле действия, что отличается от Auth Challenge)
|
||||
// Мы тестируем это отдельно, чтобы убедиться, что заголовок JWT не требуется
|
||||
|
||||
@@ -20,6 +20,7 @@ using UniVerse.Application.DTOs.Tags;
|
||||
using UniVerse.Application.DTOs.Users;
|
||||
using UniVerse.Application.Interfaces;
|
||||
using UniVerse.Domain.Enums;
|
||||
using UniVerse.Domain.Exceptions;
|
||||
using UniVerse.Infrastructure.Data;
|
||||
|
||||
namespace UniVerse.Api.Tests.Helpers;
|
||||
@@ -177,6 +178,13 @@ public class ApiWebApplicationFactory : WebApplicationFactory<Program>
|
||||
3,
|
||||
[new EnrollmentSlotRuleDto(1, 3), new EnrollmentSlotRuleDto(3, 5), new EnrollmentSlotRuleDto(4, 7)]));
|
||||
stub.GetEnrollmentsAsync(Arg.Any<int>(), Arg.Any<PaginationRequest>()).Returns(pagedLectures);
|
||||
stub.GetMyEnrollmentsIcsAsync(Arg.Any<int>()).Returns("BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n");
|
||||
stub.GetEnrollmentIcsAsync(Arg.Any<int>(), Arg.Any<int>()).Returns("BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n");
|
||||
stub.GetCalendarSubscriptionTokenAsync(Arg.Any<int>()).Returns("test-token");
|
||||
stub.GetEnrollmentsIcsBySubscriptionTokenAsync("bad-token")
|
||||
.Returns(Task.FromException<string>(new ForbiddenException("Invalid calendar subscription token.")));
|
||||
stub.GetEnrollmentsIcsBySubscriptionTokenAsync(Arg.Is<string>(token => token != "bad-token"))
|
||||
.Returns(Task.FromResult("BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n"));
|
||||
stub.GetAllAsync(Arg.Any<UserFilterRequest>()).Returns(pagedUsers);
|
||||
stub.SetRolesAsync(Arg.Any<int>(), Arg.Any<IReadOnlyCollection<UserRole>>()).Returns(Task.CompletedTask);
|
||||
stub.SetActiveAsync(Arg.Any<int>(), Arg.Any<bool>()).Returns(Task.CompletedTask);
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user