2026-10-04 12:26:26 +02:00
using System.Text.Json ;
using AIStudio.Chat ;
using AIStudio.Settings.DataModel ;
using AIStudio.Tools ;
namespace AIStudio.Tests.Chat ;
/// <summary>
2026-10-18 19:39:55 +02:00
/// Checks how the internal content a chat holds restricts where it may still send data.
2026-10-04 12:26:26 +02:00
/// </summary>
/// <remarks>
2026-10-18 19:39:55 +02:00
/// Once internal content is in a chat, mails or pages of the wiki or the intranet, every tool which
/// runs afterwards could carry it out. The chat therefore keeps the strictest restriction of every
/// source it read from, and it keeps it beyond a restart, because the content stays in the chat as
/// well.
2026-10-04 12:26:26 +02:00
/// </remarks>
[TestFixture]
public sealed class ChatThreadOutboundDataRestrictionTests
{
private const string PRIVATE_MAILBOX_ID = "5c9e2a7b-3f1d-4e8a-b6c4-2d7f9a1e3b5c" ;
private const string WORK_MAILBOX_ID = "8d1f4b6a-2e9c-4a7d-9b3e-6c5a8f2d1e4b" ;
2026-10-18 19:39:55 +02:00
private const string INTRANET_HOST = "intranet.example.org" ;
2026-10-23 09:27:22 +02:00
private const string DATA_SOURCE_ID = "3e8b1d6f-9a2c-4f7e-b5d1-6c4a9e2f8b17" ;
// A level of a newer version, or a number written into a file by hand:
private const OutboundDataRestriction UNKNOWN_RESTRICTION = ( OutboundDataRestriction ) 5 ;
2026-10-04 12:26:26 +02:00
[Test]
public void AChatWhichReadNoMailboxDemandsNothing () =>
Assert . That ( new ChatThread (). RequiredOutboundDataRestriction , Is . EqualTo ( OutboundDataRequirement . NONE ));
[TestCase(OutboundDataRestriction.UNRESTRICTED, OutboundDataRestriction.ONLY_LINKS_FROM_CHAT)]
[TestCase(OutboundDataRestriction.UNRESTRICTED, OutboundDataRestriction.ONLY_CONFIGURED_SERVICES)]
[TestCase(OutboundDataRestriction.ONLY_LINKS_FROM_CHAT, OutboundDataRestriction.ONLY_CONFIGURED_SERVICES)]
public void AStricterMailboxTightensTheChat ( OutboundDataRestriction held , OutboundDataRestriction arriving )
{
var thread = new ChatThread ();
2026-10-18 19:39:55 +02:00
thread . RequireOutboundDataRestriction ( new ( held , OutboundDataOrigin . MAILBOX , PRIVATE_MAILBOX_ID ));
thread . RequireOutboundDataRestriction ( new ( arriving , OutboundDataOrigin . MAILBOX , WORK_MAILBOX_ID ));
2026-10-04 12:26:26 +02:00
2026-10-18 19:39:55 +02:00
Assert . That ( thread . RequiredOutboundDataRestriction , Is . EqualTo ( new OutboundDataRequirement ( arriving , OutboundDataOrigin . MAILBOX , WORK_MAILBOX_ID )), "The stricter mailbox is the one which keeps a tool from running, so it is the one to name." );
2026-10-04 12:26:26 +02:00
}
[TestCase(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES, OutboundDataRestriction.ONLY_LINKS_FROM_CHAT)]
[TestCase(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES, OutboundDataRestriction.UNRESTRICTED)]
[TestCase(OutboundDataRestriction.ONLY_LINKS_FROM_CHAT, OutboundDataRestriction.UNRESTRICTED)]
public void ARestrictionStays ( OutboundDataRestriction held , OutboundDataRestriction arriving )
{
var thread = new ChatThread ();
2026-10-18 19:39:55 +02:00
thread . RequireOutboundDataRestriction ( new ( held , OutboundDataOrigin . MAILBOX , PRIVATE_MAILBOX_ID ));
thread . RequireOutboundDataRestriction ( new ( arriving , OutboundDataOrigin . MAILBOX , WORK_MAILBOX_ID ));
2026-10-04 12:26:26 +02:00
2026-10-18 19:39:55 +02:00
Assert . That ( thread . RequiredOutboundDataRestriction , Is . EqualTo ( new OutboundDataRequirement ( held , OutboundDataOrigin . MAILBOX , PRIVATE_MAILBOX_ID )), "The mail content was read by this chat. A more permissive mailbox read later cannot undo that." );
2026-10-04 12:26:26 +02:00
}
[Test]
public void OnATieTheMailboxReadFirstStaysNamed ()
{
var thread = new ChatThread ();
2026-10-18 19:39:55 +02:00
thread . RequireOutboundDataRestriction ( new ( OutboundDataRestriction . ONLY_LINKS_FROM_CHAT , OutboundDataOrigin . MAILBOX , PRIVATE_MAILBOX_ID ));
thread . RequireOutboundDataRestriction ( new ( OutboundDataRestriction . ONLY_LINKS_FROM_CHAT , OutboundDataOrigin . MAILBOX , WORK_MAILBOX_ID ));
2026-10-04 12:26:26 +02:00
2026-10-18 19:39:55 +02:00
Assert . That ( thread . RequiredOutboundDataRestriction . Reference , Is . EqualTo ( PRIVATE_MAILBOX_ID ), "Otherwise the chat would name another mailbox with every search." );
}
[Test]
public void AStricterSourceOfAnotherKindTightensTheChat ()
{
var thread = new ChatThread ();
thread . RequireOutboundDataRestriction ( new ( OutboundDataRestriction . ONLY_LINKS_FROM_CHAT , OutboundDataOrigin . MAILBOX , WORK_MAILBOX_ID ));
thread . RequireOutboundDataRestriction ( new ( OutboundDataRestriction . ONLY_CONFIGURED_SERVICES , OutboundDataOrigin . INTERNAL_WEB_PAGE , INTRANET_HOST ));
Assert . That ( thread . RequiredOutboundDataRestriction , Is . EqualTo ( new OutboundDataRequirement ( OutboundDataRestriction . ONLY_CONFIGURED_SERVICES , OutboundDataOrigin . INTERNAL_WEB_PAGE , INTRANET_HOST )), "The level decides, not the kind of source, and the stricter source is the one to name." );
}
[Test]
public void OnATieTheSourceReadFirstStaysNamedWhateverItsKind ()
{
var thread = new ChatThread ();
thread . RequireOutboundDataRestriction ( new ( OutboundDataRestriction . ONLY_LINKS_FROM_CHAT , OutboundDataOrigin . CONFLUENCE_WIKI , string . Empty ));
thread . RequireOutboundDataRestriction ( new ( OutboundDataRestriction . ONLY_LINKS_FROM_CHAT , OutboundDataOrigin . MAILBOX , WORK_MAILBOX_ID ));
thread . RequireOutboundDataRestriction ( new ( OutboundDataRestriction . ONLY_LINKS_FROM_CHAT , OutboundDataOrigin . INTERNAL_WEB_PAGE , INTRANET_HOST ));
Assert . That ( thread . RequiredOutboundDataRestriction , Is . EqualTo ( new OutboundDataRequirement ( OutboundDataRestriction . ONLY_LINKS_FROM_CHAT , OutboundDataOrigin . CONFLUENCE_WIKI , string . Empty )));
2026-10-04 12:26:26 +02:00
}
[Test]
public void ResultsWhichDemandNothingChangeNothing ()
{
var thread = new ChatThread ();
2026-10-18 19:39:55 +02:00
thread . RequireOutboundDataRestriction ( new ( OutboundDataRestriction . ONLY_CONFIGURED_SERVICES , OutboundDataOrigin . MAILBOX , PRIVATE_MAILBOX_ID ));
2026-10-04 12:26:26 +02:00
thread . RequireOutboundDataRestriction ( OutboundDataRequirement . NONE );
2026-10-23 09:27:22 +02:00
Assert . That ( thread . RequiredOutboundDataRestriction , Is . EqualTo ( new OutboundDataRequirement ( OutboundDataRestriction . ONLY_CONFIGURED_SERVICES , OutboundDataOrigin . MAILBOX , PRIVATE_MAILBOX_ID )), "A search which found nothing, say, brought nothing in and must leave the chat as it was." );
}
[TestCase(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES)]
[TestCase(OutboundDataRestriction.ONLY_LINKS_FROM_CHAT)]
[TestCase(OutboundDataRestriction.UNRESTRICTED)]
public void ARestrictionThisVersionDoesNotKnowCountsAsTheStrictest ( OutboundDataRestriction known )
{
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-23 09:27:22 +02:00
{
Assert . That ( UNKNOWN_RESTRICTION . StricterOf ( known ), Is . EqualTo ( OutboundDataRestriction . ONLY_CONFIGURED_SERVICES ));
Assert . That ( known . StricterOf ( UNKNOWN_RESTRICTION ), Is . EqualTo ( OutboundDataRestriction . ONLY_CONFIGURED_SERVICES ));
Assert . That ( UNKNOWN_RESTRICTION . StricterOf ( UNKNOWN_RESTRICTION ), Is . EqualTo ( OutboundDataRestriction . ONLY_CONFIGURED_SERVICES ));
2026-11-01 20:32:16 +01:00
}
2026-10-23 09:27:22 +02:00
}
[Test]
public void ARequirementThisVersionDoesNotKnowTightensTheChat ()
{
var thread = new ChatThread ();
thread . RequireOutboundDataRestriction ( new ( UNKNOWN_RESTRICTION , OutboundDataOrigin . MAILBOX , PRIVATE_MAILBOX_ID ));
Assert . That ( thread . RequiredOutboundDataRestriction , Is . EqualTo ( new OutboundDataRequirement ( OutboundDataRestriction . ONLY_CONFIGURED_SERVICES , OutboundDataOrigin . MAILBOX , PRIVATE_MAILBOX_ID )));
}
[TestCase(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES)]
[TestCase(OutboundDataRestriction.ONLY_LINKS_FROM_CHAT)]
[TestCase(OutboundDataRestriction.UNRESTRICTED)]
public void AChatSavedWithARestrictionThisVersionDoesNotKnowStaysStrict ( OutboundDataRestriction arriving )
{
const string CHAT_WITH_UNKNOWN_RESTRICTION = $ $"""
{
" chat_id ": " 7 a2d5f9c - 1e4 b - 4 c8a - b3f6 - 9d2e5 a1c7b84 ",
"name" : "A chat of a newer version" ,
"blocks" : [],
"required_outbound_data_restriction" : { "restriction" : 5 , "origin" : "MAILBOX" , "reference" : "{{WORK_MAILBOX_ID}}" }
}
""";
var loaded = JsonSerializer . Deserialize < ChatThread >( CHAT_WITH_UNKNOWN_RESTRICTION , WorkspaceBehaviour . JSON_OPTIONS )!;
loaded . RequireOutboundDataRestriction ( new ( arriving , OutboundDataOrigin . CONFLUENCE_WIKI , string . Empty ));
Assert . That ( loaded . RequiredOutboundDataRestriction , Is . EqualTo ( new OutboundDataRequirement ( OutboundDataRestriction . ONLY_CONFIGURED_SERVICES , OutboundDataOrigin . MAILBOX , WORK_MAILBOX_ID )), "Content read later must neither loosen the chat nor take over the name of the source." );
2026-10-04 12:26:26 +02:00
}
[Test]
public void TheRestrictionSurvivesSavingTheChat ()
{
var thread = new ChatThread { ChatId = Guid . NewGuid () };
2026-10-18 19:39:55 +02:00
thread . RequireOutboundDataRestriction ( new ( OutboundDataRestriction . ONLY_LINKS_FROM_CHAT , OutboundDataOrigin . MAILBOX , WORK_MAILBOX_ID ));
var loaded = JsonSerializer . Deserialize < ChatThread >( JsonSerializer . Serialize ( thread , WorkspaceBehaviour . JSON_OPTIONS ), WorkspaceBehaviour . JSON_OPTIONS );
Assert . That ( loaded ?. RequiredOutboundDataRestriction , Is . EqualTo ( new OutboundDataRequirement ( OutboundDataRestriction . ONLY_LINKS_FROM_CHAT , OutboundDataOrigin . MAILBOX , WORK_MAILBOX_ID )), "After a restart, the mail content is still in the chat, so the restriction has to be as well." );
}
[TestCase(OutboundDataOrigin.CONFLUENCE_WIKI, "")]
[TestCase(OutboundDataOrigin.INTERNAL_WEB_PAGE, INTRANET_HOST)]
2026-10-23 09:27:22 +02:00
[TestCase(OutboundDataOrigin.DATA_SOURCE, DATA_SOURCE_ID)]
2026-10-18 19:39:55 +02:00
public void TheOriginSurvivesSavingTheChat ( OutboundDataOrigin origin , string reference )
{
var thread = new ChatThread { ChatId = Guid . NewGuid () };
thread . RequireOutboundDataRestriction ( new ( OutboundDataRestriction . ONLY_LINKS_FROM_CHAT , origin , reference ));
2026-10-04 12:26:26 +02:00
var loaded = JsonSerializer . Deserialize < ChatThread >( JsonSerializer . Serialize ( thread , WorkspaceBehaviour . JSON_OPTIONS ), WorkspaceBehaviour . JSON_OPTIONS );
2026-10-18 19:39:55 +02:00
Assert . That ( loaded ?. RequiredOutboundDataRestriction , Is . EqualTo ( new OutboundDataRequirement ( OutboundDataRestriction . ONLY_LINKS_FROM_CHAT , origin , reference )), "Otherwise a restored chat would name a mailbox for content it read from elsewhere." );
2026-10-04 12:26:26 +02:00
}
[Test]
public void AChatSavedBeforeMailboxesExistedDemandsNothing ()
{
const string CHAT_WITHOUT_RESTRICTION = """
{
"chat_id" : "2b7c9e1f-4a6d-4c8b-9e3f-1d5a7c9b2e4f" ,
"name" : "An older chat" ,
"blocks" : []
}
""";
var loaded = JsonSerializer . Deserialize < ChatThread >( CHAT_WITHOUT_RESTRICTION , WorkspaceBehaviour . JSON_OPTIONS );
Assert . That ( loaded ?. RequiredOutboundDataRestriction , Is . EqualTo ( OutboundDataRequirement . NONE ));
}
}