diff --git a/src/ApiService/ApiService/Functions/Migrations/JinjaToScriban.cs b/src/ApiService/ApiService/Functions/Migrations/JinjaToScriban.cs index 1f6ac83fe..238976cab 100644 --- a/src/ApiService/ApiService/Functions/Migrations/JinjaToScriban.cs +++ b/src/ApiService/ApiService/Functions/Migrations/JinjaToScriban.cs @@ -39,11 +39,11 @@ public class JinjaToScriban { _log.Info($"Finding notifications to migrate"); var notifications = _context.NotificationOperations.SearchAll() - .Select(notification => { + .SelectAwait(async notification => { var (didModify, config) = notification.Config switch { TeamsTemplate => (false, notification.Config), - AdoTemplate adoTemplate => ConvertToScriban(adoTemplate), - GithubIssuesTemplate githubIssuesTemplate => ConvertToScriban(githubIssuesTemplate), + AdoTemplate adoTemplate => await JinjaTemplateAdapter.ConvertToScriban(adoTemplate), + GithubIssuesTemplate githubIssuesTemplate => await JinjaTemplateAdapter.ConvertToScriban(githubIssuesTemplate), _ => throw new NotImplementedException("Unexpected notification configuration type") }; @@ -82,153 +82,4 @@ public class JinjaToScriban { return await RequestHandling.Ok(req, new JinjaToScribanMigrationResponse(updatedNotificationsIds, failedNotificationIds)); } - - private static (bool didModify, AdoTemplate template) ConvertToScriban(AdoTemplate template) { - var didModify = false; - - if (JinjaTemplateAdapter.IsJinjaTemplate(template.Project)) { - didModify = true; - template = template with { - Project = JinjaTemplateAdapter.AdaptForScriban(template.Project) - }; - } - - foreach (var item in template.AdoFields) { - if (JinjaTemplateAdapter.IsJinjaTemplate(item.Value)) { - template.AdoFields[item.Key] = JinjaTemplateAdapter.AdaptForScriban(item.Value); - didModify = true; - } - } - - if (JinjaTemplateAdapter.IsJinjaTemplate(template.Type)) { - didModify = true; - template = template with { - Type = JinjaTemplateAdapter.AdaptForScriban(template.Type) - }; - } - - if (template.Comment != null && JinjaTemplateAdapter.IsJinjaTemplate(template.Comment)) { - didModify = true; - template = template with { - Comment = JinjaTemplateAdapter.AdaptForScriban(template.Comment) - }; - } - - foreach (var item in template.OnDuplicate.AdoFields) { - if (JinjaTemplateAdapter.IsJinjaTemplate(item.Value)) { - template.OnDuplicate.AdoFields[item.Key] = JinjaTemplateAdapter.AdaptForScriban(item.Value); - didModify = true; - } - } - - if (template.OnDuplicate.Comment != null && JinjaTemplateAdapter.IsJinjaTemplate(template.OnDuplicate.Comment)) { - didModify = true; - template = template with { - OnDuplicate = template.OnDuplicate with { - Comment = JinjaTemplateAdapter.AdaptForScriban(template.OnDuplicate.Comment) - } - }; - } - - return (didModify, template); - } - - private static (bool didModify, GithubIssuesTemplate template) ConvertToScriban(GithubIssuesTemplate template) { - var didModify = false; - - if (JinjaTemplateAdapter.IsJinjaTemplate(template.UniqueSearch.str)) { - didModify = true; - template = template with { - UniqueSearch = template.UniqueSearch with { - str = JinjaTemplateAdapter.AdaptForScriban(template.UniqueSearch.str) - } - }; - } - - if (!string.IsNullOrEmpty(template.UniqueSearch.Author) && JinjaTemplateAdapter.IsJinjaTemplate(template.UniqueSearch.Author)) { - didModify = true; - template = template with { - UniqueSearch = template.UniqueSearch with { - Author = JinjaTemplateAdapter.AdaptForScriban(template.UniqueSearch.Author) - } - }; - } - - if (JinjaTemplateAdapter.IsJinjaTemplate(template.Title)) { - didModify = true; - template = template with { - Title = JinjaTemplateAdapter.AdaptForScriban(template.Title) - }; - } - - if (JinjaTemplateAdapter.IsJinjaTemplate(template.Body)) { - didModify = true; - template = template with { - Body = JinjaTemplateAdapter.AdaptForScriban(template.Body) - }; - } - - if (!string.IsNullOrEmpty(template.OnDuplicate.Comment) && JinjaTemplateAdapter.IsJinjaTemplate(template.OnDuplicate.Comment)) { - didModify = true; - template = template with { - OnDuplicate = template.OnDuplicate with { - Comment = JinjaTemplateAdapter.AdaptForScriban(template.OnDuplicate.Comment) - } - }; - } - - if (template.OnDuplicate.Labels.Any()) { - template = template with { - OnDuplicate = template.OnDuplicate with { - Labels = template.OnDuplicate.Labels.Select(label => { - if (JinjaTemplateAdapter.IsJinjaTemplate(label)) { - didModify = true; - return JinjaTemplateAdapter.AdaptForScriban(label); - } - return label; - }).ToList() - } - }; - } - - if (template.Assignees.Any()) { - template = template with { - Assignees = template.Assignees.Select(assignee => { - if (JinjaTemplateAdapter.IsJinjaTemplate(assignee)) { - didModify = true; - return JinjaTemplateAdapter.AdaptForScriban(assignee); - } - return assignee; - }).ToList() - }; - } - - if (template.Labels.Any()) { - template = template with { - Labels = template.Labels.Select(label => { - if (JinjaTemplateAdapter.IsJinjaTemplate(label)) { - didModify = true; - return JinjaTemplateAdapter.AdaptForScriban(label); - } - return label; - }).ToList() - }; - } - - if (JinjaTemplateAdapter.IsJinjaTemplate(template.Organization)) { - didModify = true; - template = template with { - Organization = JinjaTemplateAdapter.AdaptForScriban(template.Organization) - }; - } - - if (JinjaTemplateAdapter.IsJinjaTemplate(template.Repository)) { - didModify = true; - template = template with { - Repository = JinjaTemplateAdapter.AdaptForScriban(template.Repository) - }; - } - - return (didModify, template); - } } diff --git a/src/ApiService/ApiService/Functions/ValidateScriban.cs b/src/ApiService/ApiService/Functions/ValidateScriban.cs index 64bb73a33..4e8b00335 100644 --- a/src/ApiService/ApiService/Functions/ValidateScriban.cs +++ b/src/ApiService/ApiService/Functions/ValidateScriban.cs @@ -17,19 +17,9 @@ public class ValidateScriban { return await _context.RequestHandling.NotOk(req, request.ErrorV, "ValidateTemplate"); } - var instanceUrl = _context.ServiceConfiguration.OneFuzzInstance!; - try { - var (renderer, templateRenderContext) = await GenerateTemplateRenderContext(request.OkV.Context); + return await RequestHandling.Ok(req, await JinjaTemplateAdapter.ValidateScribanTemplate(_context, _log, request.OkV.Context, request.OkV.Template)); - var renderedTemaplate = await renderer.Render(request.OkV.Template, new Uri(instanceUrl), strictRendering: true); - - var response = new TemplateValidationResponse( - renderedTemaplate, - templateRenderContext - ); - - return await RequestHandling.Ok(req, response); } catch (Exception e) { return await new RequestHandling(_log).NotOk( req, @@ -47,119 +37,4 @@ public class ValidateScriban { _ => throw new InvalidOperationException("Unsupported HTTP method"), }; } - - private async Async.Task<(NotificationsBase.Renderer, TemplateRenderContext)> GenerateTemplateRenderContext(TemplateRenderContext? templateRenderContext) { - if (templateRenderContext != null) { - _log.Info($"Using the request's TemplateRenderContext"); - } else { - _log.Info($"Generating TemplateRenderContext"); - } - - var targetUrl = templateRenderContext?.TargetUrl ?? new Uri("https://example.com/targetUrl"); - var inputUrl = templateRenderContext?.InputUrl ?? new Uri("https://example.com/inputUrl"); - var reportUrl = templateRenderContext?.ReportUrl ?? new Uri("https://example.com/reportUrl"); - var executable = "target.exe"; - var crashType = "some crash type"; - var crashSite = "some crash site"; - var callStack = new List() - { - "stack frame 0", - "stack frame 1" - }; - var callStackSha = "call stack sha"; - var inputSha = "input sha"; - var taskId = Guid.NewGuid(); - var jobId = Guid.NewGuid(); - var taskState = TaskState.Running; - var jobState = JobState.Enabled; - var os = Os.Linux; - var taskType = TaskType.LibfuzzerFuzz; - var duration = 100; - var project = "some project"; - var jobName = "job name"; - var buildName = "build name"; - var reportContainer = templateRenderContext?.ReportContainer ?? Container.Parse("example-container-name"); - var reportFileName = templateRenderContext?.ReportFilename ?? "example file name"; - var reproCmd = templateRenderContext?.ReproCmd ?? "onefuzz command to create a repro"; - var report = templateRenderContext?.Report ?? new Report( - inputUrl.ToString(), - null, - executable, - crashType, - crashSite, - callStack, - callStackSha, - inputSha, - null, - taskId, - jobId, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null - ); - - var task = new Task( - jobId, - taskId, - taskState, - os, - templateRenderContext?.Task ?? new TaskConfig( - jobId, - null, - new TaskDetails( - taskType, - duration - ) - ) - ); - - var job = new Job( - jobId, - jobState, - templateRenderContext?.Job ?? new JobConfig( - project, - jobName, - buildName, - duration, - null - ) - ); - - var renderer = await NotificationsBase.Renderer.ConstructRenderer( - _context, - reportContainer, - reportFileName, - report, - _log, - task, - job, - targetUrl, - inputUrl, - reportUrl, - scribanOnlyOverride: true - ); - - templateRenderContext ??= new TemplateRenderContext( - report, - task.Config, - job.Config, - reportUrl, - inputUrl, - targetUrl, - reportContainer, - reportFileName, - reproCmd - ); - - return (renderer, templateRenderContext); - } } diff --git a/src/ApiService/ApiService/onefuzzlib/NotificationOperations.cs b/src/ApiService/ApiService/onefuzzlib/NotificationOperations.cs index 14ef941e4..fa79284c9 100644 --- a/src/ApiService/ApiService/onefuzzlib/NotificationOperations.cs +++ b/src/ApiService/ApiService/onefuzzlib/NotificationOperations.cs @@ -91,6 +91,11 @@ public class NotificationOperations : Orm, INotificationOperations return OneFuzzResult.Error(ErrorCode.INVALID_REQUEST, "invalid container"); } + if (await _context.FeatureManagerSnapshot.IsEnabledAsync(FeatureFlagConstants.EnableScribanOnly) && + !await JinjaTemplateAdapter.IsValidScribanNotificationTemplate(_context, _logTracer, config)) { + return OneFuzzResult.Error(ErrorCode.INVALID_REQUEST, "The notification config is not a valid scriban template"); + } + if (replaceExisting) { var existing = this.SearchByRowKeys(new[] { container.String }); await foreach (var existingEntry in existing) { diff --git a/src/ApiService/ApiService/onefuzzlib/notifications/JinjaTemplateAdapter.cs b/src/ApiService/ApiService/onefuzzlib/notifications/JinjaTemplateAdapter.cs index 48ef27a44..d735b40a7 100644 --- a/src/ApiService/ApiService/onefuzzlib/notifications/JinjaTemplateAdapter.cs +++ b/src/ApiService/ApiService/onefuzzlib/notifications/JinjaTemplateAdapter.cs @@ -11,4 +11,351 @@ public class JinjaTemplateAdapter { .Replace("{%", "{{") .Replace("%}", "}}"); } + + public static async Async.Task IsValidScribanNotificationTemplate(IOnefuzzContext context, ILogTracer log, NotificationTemplate template) { + try { + var (didModify, _) = template switch { + TeamsTemplate => (false, template), + AdoTemplate adoTemplate => await ConvertToScriban(adoTemplate, attemptRender: true, context, log), + GithubIssuesTemplate githubTemplate => await ConvertToScriban(githubTemplate, attemptRender: true, context, log), + _ => throw new ArgumentOutOfRangeException(nameof(template), "Unexpected notification template type") + }; + + if (!didModify) { + return true; + } + return false; + } catch (Exception e) { + log.Exception(e); + return false; + } + } + + public static async Async.Task ValidateScribanTemplate(IOnefuzzContext context, ILogTracer log, TemplateRenderContext? renderContext, string template) { + var instanceUrl = context.ServiceConfiguration.OneFuzzInstance!; + + var (renderer, templateRenderContext) = await GenerateTemplateRenderContext(context, log, renderContext); + + var renderedTemaplate = await renderer.Render(template, new Uri(instanceUrl), strictRendering: true); + + return new TemplateValidationResponse( + renderedTemaplate, + templateRenderContext + ); + } + + private static async Async.Task<(NotificationsBase.Renderer, TemplateRenderContext)> GenerateTemplateRenderContext(IOnefuzzContext context, ILogTracer log, TemplateRenderContext? templateRenderContext) { + if (templateRenderContext != null) { + log.Info($"Using custom TemplateRenderContext"); + } else { + log.Info($"Generating TemplateRenderContext"); + } + + var targetUrl = templateRenderContext?.TargetUrl ?? new Uri("https://example.com/targetUrl"); + var inputUrl = templateRenderContext?.InputUrl ?? new Uri("https://example.com/inputUrl"); + var reportUrl = templateRenderContext?.ReportUrl ?? new Uri("https://example.com/reportUrl"); + var executable = "target.exe"; + var crashType = "some crash type"; + var crashSite = "some crash site"; + var callStack = new List() + { + "stack frame 0", + "stack frame 1" + }; + var callStackSha = "call stack sha"; + var inputSha = "input sha"; + var taskId = Guid.NewGuid(); + var jobId = Guid.NewGuid(); + var taskState = TaskState.Running; + var jobState = JobState.Enabled; + var os = Os.Linux; + var taskType = TaskType.LibfuzzerFuzz; + var duration = 100; + var project = "some project"; + var jobName = "job name"; + var buildName = "build name"; + var reportContainer = templateRenderContext?.ReportContainer ?? Container.Parse("example-container-name"); + var reportFileName = templateRenderContext?.ReportFilename ?? "example file name"; + var reproCmd = templateRenderContext?.ReproCmd ?? "onefuzz command to create a repro"; + var report = templateRenderContext?.Report ?? new Report( + inputUrl.ToString(), + null, + executable, + crashType, + crashSite, + callStack, + callStackSha, + inputSha, + null, + taskId, + jobId, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + + var task = new Task( + jobId, + taskId, + taskState, + os, + templateRenderContext?.Task ?? new TaskConfig( + jobId, + null, + new TaskDetails( + taskType, + duration + ) + ) + ); + + var job = new Job( + jobId, + jobState, + templateRenderContext?.Job ?? new JobConfig( + project, + jobName, + buildName, + duration, + null + ) + ); + + var renderer = await NotificationsBase.Renderer.ConstructRenderer( + context, + reportContainer, + reportFileName, + report, + log, + task, + job, + targetUrl, + inputUrl, + reportUrl, + scribanOnlyOverride: true + ); + + templateRenderContext ??= new TemplateRenderContext( + report, + task.Config, + job.Config, + reportUrl, + inputUrl, + targetUrl, + reportContainer, + reportFileName, + reproCmd + ); + + return (renderer, templateRenderContext); + } + + public async static Async.Task<(bool didModify, AdoTemplate template)> ConvertToScriban(AdoTemplate template, bool attemptRender = false, IOnefuzzContext? context = null, ILogTracer? log = null) { + if (attemptRender) { + context = context.EnsureNotNull("Required to render"); + log = log.EnsureNotNull("Required to render"); + } + + var didModify = false; + + if (JinjaTemplateAdapter.IsJinjaTemplate(template.Project)) { + didModify = true; + template = template with { + Project = JinjaTemplateAdapter.AdaptForScriban(template.Project) + }; + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, template.Project).IgnoreResult(); + } + + foreach (var item in template.AdoFields) { + if (JinjaTemplateAdapter.IsJinjaTemplate(item.Value)) { + template.AdoFields[item.Key] = JinjaTemplateAdapter.AdaptForScriban(item.Value); + didModify = true; + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, item.Value).IgnoreResult(); + } + } + + if (JinjaTemplateAdapter.IsJinjaTemplate(template.Type)) { + didModify = true; + template = template with { + Type = JinjaTemplateAdapter.AdaptForScriban(template.Type) + }; + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, template.Type).IgnoreResult(); + } + + if (template.Comment != null) { + if (JinjaTemplateAdapter.IsJinjaTemplate(template.Comment)) { + didModify = true; + template = template with { + Comment = JinjaTemplateAdapter.AdaptForScriban(template.Comment) + }; + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, template.Comment).IgnoreResult(); + } + } + + foreach (var item in template.OnDuplicate.AdoFields) { + if (JinjaTemplateAdapter.IsJinjaTemplate(item.Value)) { + template.OnDuplicate.AdoFields[item.Key] = JinjaTemplateAdapter.AdaptForScriban(item.Value); + didModify = true; + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, item.Value).IgnoreResult(); + } + } + + if (template.OnDuplicate.Comment != null) { + if (JinjaTemplateAdapter.IsJinjaTemplate(template.OnDuplicate.Comment)) { + didModify = true; + template = template with { + OnDuplicate = template.OnDuplicate with { + Comment = JinjaTemplateAdapter.AdaptForScriban(template.OnDuplicate.Comment) + } + }; + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, template.OnDuplicate.Comment).IgnoreResult(); + } + } + + return (didModify, template); + } + + public async static Async.Task<(bool didModify, GithubIssuesTemplate template)> ConvertToScriban(GithubIssuesTemplate template, bool attemptRender = false, IOnefuzzContext? context = null, ILogTracer? log = null) { + if (attemptRender) { + context = context.EnsureNotNull("Required to render"); + log = log.EnsureNotNull("Required to render"); + } + + var didModify = false; + + if (JinjaTemplateAdapter.IsJinjaTemplate(template.UniqueSearch.str)) { + didModify = true; + template = template with { + UniqueSearch = template.UniqueSearch with { + str = JinjaTemplateAdapter.AdaptForScriban(template.UniqueSearch.str) + } + }; + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, template.UniqueSearch.str).IgnoreResult(); + } + + + if (!string.IsNullOrEmpty(template.UniqueSearch.Author)) { + if (JinjaTemplateAdapter.IsJinjaTemplate(template.UniqueSearch.Author)) { + didModify = true; + template = template with { + UniqueSearch = template.UniqueSearch with { + Author = JinjaTemplateAdapter.AdaptForScriban(template.UniqueSearch.Author) + } + }; + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, template.UniqueSearch.Author).IgnoreResult(); + } + } + + if (JinjaTemplateAdapter.IsJinjaTemplate(template.Title)) { + didModify = true; + template = template with { + Title = JinjaTemplateAdapter.AdaptForScriban(template.Title) + }; + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, template.Title).IgnoreResult(); + } + + if (JinjaTemplateAdapter.IsJinjaTemplate(template.Body)) { + didModify = true; + template = template with { + Body = JinjaTemplateAdapter.AdaptForScriban(template.Body) + }; + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, template.Body).IgnoreResult(); + } + + if (!string.IsNullOrEmpty(template.OnDuplicate.Comment)) { + if (JinjaTemplateAdapter.IsJinjaTemplate(template.OnDuplicate.Comment)) { + didModify = true; + template = template with { + OnDuplicate = template.OnDuplicate with { + Comment = JinjaTemplateAdapter.AdaptForScriban(template.OnDuplicate.Comment) + } + }; + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, template.OnDuplicate.Comment).IgnoreResult(); + } + } + + if (template.OnDuplicate.Labels.Any()) { + template = template with { + OnDuplicate = template.OnDuplicate with { + Labels = template.OnDuplicate.Labels.ToAsyncEnumerable().SelectAwait(async label => { + if (JinjaTemplateAdapter.IsJinjaTemplate(label)) { + didModify = true; + return JinjaTemplateAdapter.AdaptForScriban(label); + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, label).IgnoreResult(); + } + return label; + }).ToEnumerable().ToList() + } + }; + } + + if (template.Assignees.Any()) { + template = template with { + Assignees = template.Assignees.ToAsyncEnumerable().SelectAwait(async assignee => { + if (JinjaTemplateAdapter.IsJinjaTemplate(assignee)) { + didModify = true; + return JinjaTemplateAdapter.AdaptForScriban(assignee); + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, assignee).IgnoreResult(); + } + return assignee; + }).ToEnumerable().ToList() + }; + } + + if (template.Labels.Any()) { + template = template with { + Labels = template.Labels.ToAsyncEnumerable().SelectAwait(async label => { + if (JinjaTemplateAdapter.IsJinjaTemplate(label)) { + didModify = true; + return JinjaTemplateAdapter.AdaptForScriban(label); + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, label).IgnoreResult(); + } + return label; + }).ToEnumerable().ToList() + }; + } + + if (JinjaTemplateAdapter.IsJinjaTemplate(template.Organization)) { + didModify = true; + template = template with { + Organization = JinjaTemplateAdapter.AdaptForScriban(template.Organization) + }; + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, template.Organization).IgnoreResult(); + } + + if (JinjaTemplateAdapter.IsJinjaTemplate(template.Repository)) { + didModify = true; + template = template with { + Repository = JinjaTemplateAdapter.AdaptForScriban(template.Repository) + }; + } else if (attemptRender) { + await ValidateScribanTemplate(context!, log!, null, template.Repository).IgnoreResult(); + } + + return (didModify, template); + } } diff --git a/src/ApiService/IntegrationTests/Fakes/TestContext.cs b/src/ApiService/IntegrationTests/Fakes/TestContext.cs index 0afec423c..e69ae1955 100644 --- a/src/ApiService/IntegrationTests/Fakes/TestContext.cs +++ b/src/ApiService/IntegrationTests/Fakes/TestContext.cs @@ -41,6 +41,7 @@ public sealed class TestContext : IOnefuzzContext { UserCredentials = new UserCredentials(logTracer, ConfigOperations); NotificationOperations = new NotificationOperations(logTracer, this); SecretsOperations = new TestSecretsOperations(Creds, ServiceConfiguration); + FeatureManagerSnapshot = new TestFeatureManagerSnapshot(); } public TestEvents Events { get; set; } = new(); @@ -92,6 +93,9 @@ public sealed class TestContext : IOnefuzzContext { public ISecretsOperations SecretsOperations { get; } + public IFeatureManagerSnapshot FeatureManagerSnapshot { get; } + + // -- Remainder not implemented -- public IConfig Config => throw new System.NotImplementedException(); @@ -129,7 +133,6 @@ public sealed class TestContext : IOnefuzzContext { public ITeams Teams => throw new NotImplementedException(); public IGithubIssues GithubIssues => throw new NotImplementedException(); public IAdo Ado => throw new NotImplementedException(); - public IFeatureManagerSnapshot FeatureManagerSnapshot => throw new NotImplementedException(); public IConfigurationRefresher ConfigurationRefresher => throw new NotImplementedException(); } diff --git a/src/ApiService/IntegrationTests/Fakes/TestFeatureManagerSnapshot.cs b/src/ApiService/IntegrationTests/Fakes/TestFeatureManagerSnapshot.cs new file mode 100644 index 000000000..7ca3584ae --- /dev/null +++ b/src/ApiService/IntegrationTests/Fakes/TestFeatureManagerSnapshot.cs @@ -0,0 +1,30 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.FeatureManagement; + +namespace IntegrationTests.Fakes; + +public class TestFeatureManagerSnapshot : IFeatureManagerSnapshot { + + private static ConcurrentDictionary FeatureFlags = new(); + public IAsyncEnumerable GetFeatureNamesAsync() { + throw new System.NotImplementedException(); + } + + public Task IsEnabledAsync(string feature) { + return Task.FromResult(FeatureFlags.ContainsKey(feature) && FeatureFlags.TryGetValue(feature, out var enabled) && enabled); + } + + public Task IsEnabledAsync(string feature, TContext context) { + throw new System.NotImplementedException(); + } + + public static void AddFeatureFlag(string featureName, bool enabled = false) { + var _ = FeatureFlags.TryAdd(featureName, enabled); + } + + public static void SetFeatureFlag(string featureName, bool enabled) { + var _ = FeatureFlags.TryUpdate(featureName, enabled, !enabled); + } +}