Add calendar integrations and collaboration updates
Some checks failed
Backend CI/CD / build_and_deploy (push) Has been cancelled
Frontend CI/CD / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-05-05 15:25:53 -04:00
parent c49f03ec06
commit b66c10b681
82 changed files with 8420 additions and 2048 deletions

View File

@@ -0,0 +1,63 @@
using Socialize.Api.Modules.CalendarIntegrations.Services;
namespace Socialize.Tests.CalendarIntegrations;
public class CalendarExportFeedTests
{
[Fact]
public void Token_regeneration_changes_private_feed_secret()
{
string firstToken = CalendarExportFeedTokenService.GenerateToken();
string secondToken = CalendarExportFeedTokenService.GenerateToken();
Assert.NotEqual(firstToken, secondToken);
Assert.NotEqual(
CalendarExportFeedTokenService.HashToken(firstToken),
CalendarExportFeedTokenService.HashToken(secondToken));
}
[Fact]
public void HashToken_allows_token_authorization_boundary_checks_without_plaintext_comparison()
{
string token = CalendarExportFeedTokenService.GenerateToken();
string storedHash = CalendarExportFeedTokenService.HashToken(token);
Assert.Equal(storedHash, CalendarExportFeedTokenService.HashToken(token));
Assert.NotEqual(storedHash, CalendarExportFeedTokenService.HashToken(CalendarExportFeedTokenService.GenerateToken()));
}
[Fact]
public void Build_emits_valid_ics_for_user_work_without_sensitive_discussion_details()
{
CalendarExportFeedBuilder builder = new();
string ics = builder.Build(
"Socialize my work",
[
new CalendarExportFeedEvent(
"content-1@socialize",
"Launch reel",
new DateTimeOffset(2026, 5, 10, 9, 0, 0, TimeSpan.Zero),
new DateTimeOffset(2026, 5, 10, 9, 30, 0, TimeSpan.Zero),
IsAllDay: false,
"Status: Draft\nWorkspace: Brand A\nClient: Client\nCampaign: Spring launch",
"https://app.test/app/content/content-1"),
new CalendarExportFeedEvent(
"approval-1@socialize",
"Approval due: Launch reel",
new DateTimeOffset(2026, 5, 12, 0, 0, 0, TimeSpan.Zero),
new DateTimeOffset(2026, 5, 13, 0, 0, 0, TimeSpan.Zero),
IsAllDay: true,
"Stage: Client review\nState: Pending\nWorkspace: Brand A",
"https://app.test/app/content/content-1"),
]);
Assert.StartsWith("BEGIN:VCALENDAR", ics);
Assert.Contains("SUMMARY:Launch reel", ics);
Assert.Contains("SUMMARY:Approval due: Launch reel", ics);
Assert.Contains("DTSTART:20260510T090000Z", ics);
Assert.Contains("DTSTART;VALUE=DATE:20260512", ics);
Assert.DoesNotContain("approval-token", ics);
Assert.DoesNotContain("Mother's Day", ics);
Assert.EndsWith("END:VCALENDAR" + Environment.NewLine, ics);
}
}

View File

@@ -0,0 +1,16 @@
using Socialize.Api.Modules.CalendarIntegrations.Services;
namespace Socialize.Tests.CalendarIntegrations;
public class CalendarImportSyncServiceTests
{
[Fact]
public void NormalizeSyncError_truncates_errors_to_stored_length()
{
string message = new('x', 3000);
string normalized = CalendarImportSyncService.NormalizeSyncError(message);
Assert.Equal(2048, normalized.Length);
}
}

View File

@@ -0,0 +1,87 @@
using Socialize.Api.Modules.CalendarIntegrations.Data;
using Socialize.Api.Modules.CalendarIntegrations.Handlers;
using Socialize.Api.Modules.CalendarIntegrations.Services;
namespace Socialize.Tests.CalendarIntegrations;
public class CalendarSourceRulesTests
{
[Theory]
[InlineData(CalendarSourceScopes.Organization, true, false, true)]
[InlineData(CalendarSourceScopes.Organization, false, true, false)]
[InlineData(CalendarSourceScopes.Workspace, false, true, true)]
[InlineData(CalendarSourceScopes.Workspace, true, false, false)]
public void CanManageScope_uses_scope_specific_shared_calendar_permissions(
string scope,
bool canManageOrganizationCalendars,
bool canManageWorkspaceCalendars,
bool expected)
{
Guid currentUserId = Guid.NewGuid();
bool actual = CalendarSourceRules.CanManageScope(
scope,
canManageOrganizationCalendars,
canManageWorkspaceCalendars,
currentUserId,
sourceUserId: null);
Assert.Equal(expected, actual);
}
[Fact]
public void CanManageScope_allows_only_owner_for_user_sources()
{
Guid currentUserId = Guid.NewGuid();
Assert.True(CalendarSourceRules.CanManageScope(
CalendarSourceScopes.User,
canManageOrganizationCalendars: false,
canManageWorkspaceCalendars: false,
currentUserId,
currentUserId));
Assert.False(CalendarSourceRules.CanManageScope(
CalendarSourceScopes.User,
canManageOrganizationCalendars: true,
canManageWorkspaceCalendars: true,
currentUserId,
Guid.NewGuid()));
}
[Fact]
public void Workspace_context_marks_inherited_organization_sources_read_only()
{
Guid organizationId = Guid.NewGuid();
CalendarSource organizationSource = new()
{
Id = Guid.NewGuid(),
Scope = CalendarSourceScopes.Organization,
OrganizationId = organizationId,
DisplayTitle = "Public holidays",
Color = "#2F80ED",
Category = "public-holiday",
InheritanceMode = CalendarSourceInheritanceModes.Required,
};
CalendarSource workspaceSource = new()
{
Id = Guid.NewGuid(),
Scope = CalendarSourceScopes.Workspace,
WorkspaceId = Guid.NewGuid(),
DisplayTitle = "Campaign moments",
Color = "#27AE60",
Category = "marketing-moment",
};
CalendarSourceDto inheritedDto = CalendarSourceDto.FromSource(
organizationSource,
CalendarSourceRules.IsInheritedOrganizationSource(organizationSource, organizationId));
CalendarSourceDto workspaceDto = CalendarSourceDto.FromSource(
workspaceSource,
CalendarSourceRules.IsInheritedOrganizationSource(workspaceSource, organizationId));
Assert.True(inheritedDto.IsReadOnly);
Assert.False(workspaceDto.IsReadOnly);
}
}

View File

@@ -0,0 +1,132 @@
using Socialize.Api.Modules.CalendarIntegrations.Services;
namespace Socialize.Tests.CalendarIntegrations;
public class IcsCalendarParserTests
{
private readonly IcsCalendarParser _parser = new();
[Fact]
public void Parse_preserves_all_day_calendar_dates()
{
string ics = """
BEGIN:VCALENDAR
BEGIN:VEVENT
UID:christmas-eve
SUMMARY:Christmas Eve
DTSTART;VALUE=DATE:20261224
DTEND;VALUE=DATE:20261225
END:VEVENT
END:VCALENDAR
""";
ParsedCalendarEvent calendarEvent = Assert.Single(_parser.Parse(
ics,
new DateOnly(2026, 12, 1),
new DateOnly(2026, 12, 31)));
Assert.True(calendarEvent.IsAllDay);
Assert.Equal(new DateOnly(2026, 12, 24), calendarEvent.StartDate);
Assert.Equal(new DateOnly(2026, 12, 25), calendarEvent.EndDate);
Assert.Null(calendarEvent.StartUtc);
}
[Fact]
public void Parse_keeps_floating_timed_events_as_local_values_without_utc_conversion()
{
string ics = """
BEGIN:VCALENDAR
BEGIN:VEVENT
UID:floating
SUMMARY:Local planning
DTSTART:20260510T090000
DTEND:20260510T100000
END:VEVENT
END:VCALENDAR
""";
ParsedCalendarEvent calendarEvent = Assert.Single(_parser.Parse(
ics,
new DateOnly(2026, 5, 1),
new DateOnly(2026, 5, 31)));
Assert.False(calendarEvent.IsAllDay);
Assert.True(calendarEvent.IsFloatingTime);
Assert.Equal(new DateTime(2026, 5, 10, 9, 0, 0), calendarEvent.StartLocalDateTime);
Assert.Null(calendarEvent.StartUtc);
}
[Fact]
public void Parse_converts_timezone_bearing_timed_events_when_timezone_is_known()
{
string ics = """
BEGIN:VCALENDAR
BEGIN:VEVENT
UID:timed
SUMMARY:Launch
DTSTART;TZID=America/Toronto:20260510T090000
DTEND;TZID=America/Toronto:20260510T100000
END:VEVENT
END:VCALENDAR
""";
ParsedCalendarEvent calendarEvent = Assert.Single(_parser.Parse(
ics,
new DateOnly(2026, 5, 1),
new DateOnly(2026, 5, 31)));
Assert.False(calendarEvent.IsFloatingTime);
Assert.Equal("America/Toronto", calendarEvent.TimeZoneId);
Assert.Equal(TimeSpan.Zero, calendarEvent.StartUtc?.Offset);
Assert.Equal(13, calendarEvent.StartUtc?.Hour);
}
[Fact]
public void Parse_expands_yearly_recurrence_inside_requested_range()
{
string ics = """
BEGIN:VCALENDAR
BEGIN:VEVENT
UID:mothers-day
SUMMARY:Mother's Day
DTSTART;VALUE=DATE:20240512
DTEND;VALUE=DATE:20240513
RRULE:FREQ=YEARLY;COUNT=5
END:VEVENT
END:VCALENDAR
""";
IReadOnlyCollection<ParsedCalendarEvent> events = _parser.Parse(
ics,
new DateOnly(2026, 1, 1),
new DateOnly(2027, 12, 31));
Assert.Collection(
events,
first => Assert.Equal(new DateOnly(2026, 5, 12), first.StartDate),
second => Assert.Equal(new DateOnly(2027, 5, 12), second.StartDate));
Assert.All(events, calendarEvent => Assert.Equal("mothers-day", calendarEvent.RecurrenceId));
}
[Fact]
public void Parse_unfolds_folded_text_lines()
{
string ics = """
BEGIN:VCALENDAR
BEGIN:VEVENT
UID:folded
SUMMARY:Long
title
DTSTART;VALUE=DATE:20260510
END:VEVENT
END:VCALENDAR
""";
ParsedCalendarEvent calendarEvent = Assert.Single(_parser.Parse(
ics,
new DateOnly(2026, 5, 1),
new DateOnly(2026, 5, 31)));
Assert.Equal("Longtitle", calendarEvent.Title);
}
}