diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f71789602..81cc7af84 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,10 +2,10 @@ name: "CodeQL" on: push: - branches: [ "main" ] + branches: [ "main", "v8" ] pull_request: # The branches below must be a subset of the branches above - branches: [ "main" ] + branches: [ "main", "v8" ] schedule: - cron: '37 20 * * 3' @@ -26,6 +26,16 @@ jobs: # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: + # The following step is required in order to use .NET 8 pre-release. + # We can remove if using an officially supported .NET version. + # See https://github.com/github/codeql-action/issues/757#issuecomment-977546999 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.0.x + include-prerelease: true + - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml index 5427d515b..f25b99e14 100644 --- a/.github/workflows/dotnet-test.yml +++ b/.github/workflows/dotnet-test.yml @@ -1,9 +1,9 @@ name: Build and Test on windows, macos and ubuntu on: push: - branches: [ main ] + branches: [ main, v8 ] pull_request: - branches: [ main ] + branches: [ main, v8 ] types: [opened, synchronize, reopened] workflow_dispatch: jobs: @@ -20,8 +20,7 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | - 6.0.x - 5.0.x + 8.0.x - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index ec6fb150b..3af06de13 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,4 +1,4 @@ -name: Pack and publish nugets +name: Pack and publish on: release: @@ -6,19 +6,19 @@ on: - published jobs: - build-pack: + release-nugets: + if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Install dotnet6 - uses: actions/setup-dotnet@v4 + - name: Install dotnet8 + uses: actions/setup-dotnet@v3 with: dotnet-version: | - 5.0.x - 6.0.x + 8.0.x - name: Install deps run: | dotnet restore @@ -33,4 +33,37 @@ jobs: dotnet --version - name: Publish run: | - dotnet nuget push src/**/bin/Release/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} \ No newline at end of file + dotnet nuget push src/**/bin/Release/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} + release-upgrade-tool: + if: startsWith(github.ref, 'refs/tags/altinn-app-cli') + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./cli-tools/altinn-app-cli + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Install dotnet8 + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.0.x + - name: Build bundles + run: | + make bundles + - name: Upload files to release + uses: softprops/action-gh-release@v1 + with: + files: | + publish/archives/osx-x64.tar.gz + publish/archives/osx-arm64.tar.gz + publish/archives/linux-x64.tar.gz + publish/archives/linux-arm64.tar.gz + publish/archives/win-x64.zip + publish/archives/osx-x64.tar.gz.sha512 + publish/archives/osx-arm64.tar.gz.sha512 + publish/archives/linux-x64.tar.gz.sha512 + publish/archives/linux-arm64.tar.gz.sha512 + publish/archives/win-x64.zip.sha512 + \ No newline at end of file diff --git a/.github/workflows/test-and-analyze-fork.yml b/.github/workflows/test-and-analyze-fork.yml index 240098178..1fde74299 100644 --- a/.github/workflows/test-and-analyze-fork.yml +++ b/.github/workflows/test-and-analyze-fork.yml @@ -1,7 +1,7 @@ name: Code test and analysis (fork) on: pull_request: - branches: [ main ] + branches: [ main, v8 ] types: [opened, synchronize, reopened, ready_for_review] jobs: test: @@ -13,8 +13,7 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | - 6.0.x - 5.0.x + 8.0.x - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis diff --git a/.github/workflows/test-and-analyze.yml b/.github/workflows/test-and-analyze.yml index 982770cac..fb8ae861e 100644 --- a/.github/workflows/test-and-analyze.yml +++ b/.github/workflows/test-and-analyze.yml @@ -1,9 +1,9 @@ name: Code test and analysis on: push: - branches: [ main ] + branches: [ main, v8 ] pull_request: - branches: [ main ] + branches: [ main, v8 ] types: [opened, synchronize, reopened] workflow_dispatch: jobs: @@ -19,8 +19,7 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | - 6.0.x - 5.0.x + 8.0.x - name: Set up JDK 11 uses: actions/setup-java@v4 with: diff --git a/cli-tools/altinn-app-cli/Makefile b/cli-tools/altinn-app-cli/Makefile new file mode 100644 index 000000000..6cae575a1 --- /dev/null +++ b/cli-tools/altinn-app-cli/Makefile @@ -0,0 +1,42 @@ +# Path: Makefile + +build: + dotnet build + +executable-osx-x64: + dotnet publish -c Release -o publish/osx-x64 -r osx-x64 --self-contained + +executable-osx-arm64: + dotnet publish -c Release -o publish/osx-arm64 -r osx-arm64 --self-contained + +executable-win-x64: + dotnet publish -c Release -o publish/win-x64 -r win-x64 --self-contained + +executable-linux-x64: + dotnet publish -c Release -o publish/linux-x64 -r linux-x64 --self-contained + +executable-linux-arm64: + dotnet publish -c Release -o publish/linux-arm64 -r linux-arm64 --self-contained + +executables: executable-osx-x64 executable-osx-arm64 executable-win-x64 executable-linux-x64 executable-linux-arm64 + +archives: + mkdir -p publish/archives + tar -czvf publish/archives/osx-x64.tar.gz publish/osx-x64/altinn-app-cli + tar -czvf publish/archives/osx-arm64.tar.gz publish/osx-arm64/altinn-app-cli + tar -czvf publish/archives/linux-x64.tar.gz publish/linux-x64/altinn-app-cli + tar -czvf publish/archives/linux-arm64.tar.gz publish/linux-arm64/altinn-app-cli + zip -r publish/archives/win-x64.zip publish/win-x64/altinn-app-cli.exe + +checksums: + sha512sum publish/archives/osx-x64.tar.gz > publish/archives/osx-x64.tar.gz.sha512 + sha512sum publish/archives/osx-arm64.tar.gz > publish/archives/osx-arm64.tar.gz.sha512 + sha512sum publish/archives/linux-x64.tar.gz > publish/archives/linux-x64.tar.gz.sha512 + sha512sum publish/archives/linux-arm64.tar.gz > publish/archives/linux-arm64.tar.gz.sha512 + sha512sum publish/archives/win-x64.zip > publish/archives/win-x64.zip.sha512 + +bundles: executables archives checksums + +clean: + dotnet clean + rm -rf publish/ \ No newline at end of file diff --git a/cli-tools/altinn-app-cli/Program.cs b/cli-tools/altinn-app-cli/Program.cs new file mode 100644 index 000000000..a60e612a1 --- /dev/null +++ b/cli-tools/altinn-app-cli/Program.cs @@ -0,0 +1,280 @@ +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Reflection; +using altinn_app_cli.v7Tov8.AppSettingsRewriter; +using altinn_app_cli.v7Tov8.CodeRewriters; +using altinn_app_cli.v7Tov8.DockerfileRewriters; +using altinn_app_cli.v7Tov8.ProcessRewriter; +using altinn_app_cli.v7Tov8.ProjectChecks; +using altinn_app_cli.v7Tov8.ProjectRewriters; +using Microsoft.Build.Locator; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.MSBuild; + +namespace altinn_app_upgrade_cli; + +class Program +{ + static async Task Main(string[] args) + { + int returnCode = 0; + var projectFolderOption = new Option(name: "--folder", description: "The project folder to read", getDefaultValue: () => "CurrentDirectory"); + var projectFileOption = new Option(name: "--project", description: "The project file to read relative to --folder", getDefaultValue: () => "App/App.csproj"); + var processFileOption = new Option(name: "--process", description: "The process file to read relative to --folder", getDefaultValue: () => "App/config/process/process.bpmn"); + var appSettingsFolderOption = new Option(name: "--appsettings-folder", description: "The folder where the appsettings.*.json files are located", getDefaultValue: () => "App"); + var targetVersionOption = new Option(name: "--target-version", description: "The target version to upgrade to", getDefaultValue: () => "8.0.0-preview.11"); + var targetFrameworkOption = new Option(name: "--target-framework", description: "The target dotnet framework version to upgrade to", getDefaultValue: () => "net8.0"); + var skipCsprojUpgradeOption = new Option(name: "--skip-csproj-upgrade", description: "Skip csproj file upgrade", getDefaultValue: () => false); + var skipDockerUpgradeOption = new Option(name: "--skip-dockerfile-upgrade", description: "Skip Dockerfile upgrade", getDefaultValue: () => false); + var skipCodeUpgradeOption = new Option(name: "--skip-code-upgrade", description: "Skip code upgrade", getDefaultValue: () => false); + var skipProcessUpgradeOption = new Option(name: "--skip-process-upgrade", description: "Skip process file upgrade", getDefaultValue: () => false); + var skipAppSettingsUpgradeOption = new Option(name: "--skip-appsettings-upgrade", description: "Skip appsettings file upgrade", getDefaultValue: () => false); + var rootCommand = new RootCommand("Command line interface for working with Altinn 3 Applications"); + var upgradeCommand = new Command("upgrade", "Upgrade an app from v7 to v8") + { + projectFolderOption, + projectFileOption, + processFileOption, + appSettingsFolderOption, + targetVersionOption, + targetFrameworkOption, + skipCsprojUpgradeOption, + skipDockerUpgradeOption, + skipCodeUpgradeOption, + skipProcessUpgradeOption, + skipAppSettingsUpgradeOption, + }; + rootCommand.AddCommand(upgradeCommand); + var versionCommand = new Command("version", "Print version of altinn-app-cli"); + rootCommand.AddCommand(versionCommand); + + upgradeCommand.SetHandler( + async (InvocationContext context) => + { + var projectFolder = context.ParseResult.GetValueForOption(projectFolderOption)!; + var projectFile = context.ParseResult.GetValueForOption(projectFileOption)!; + var processFile = context.ParseResult.GetValueForOption(processFileOption)!; + var appSettingsFolder = context.ParseResult.GetValueForOption(appSettingsFolderOption)!; + var targetVersion = context.ParseResult.GetValueForOption(targetVersionOption)!; + var targetFramework = context.ParseResult.GetValueForOption(targetFrameworkOption)!; + var skipCodeUpgrade = context.ParseResult.GetValueForOption(skipCodeUpgradeOption); + var skipProcessUpgrade = context.ParseResult.GetValueForOption(skipProcessUpgradeOption); + var skipCsprojUpgrade = context.ParseResult.GetValueForOption(skipCsprojUpgradeOption); + var skipDockerUpgrade = context.ParseResult.GetValueForOption(skipDockerUpgradeOption); + var skipAppSettingsUpgrade = context.ParseResult.GetValueForOption(skipAppSettingsUpgradeOption); + + if (projectFolder == "CurrentDirectory") + { + projectFolder = Directory.GetCurrentDirectory(); + } + + if (File.Exists(projectFolder)) + { + Console.WriteLine($"Project folder {projectFolder} does not exist. Please supply location of project with --folder [path/to/project]"); + returnCode = 1; + return; + } + + FileAttributes attr = File.GetAttributes(projectFolder); + if ((attr & FileAttributes.Directory) != FileAttributes.Directory) + { + Console.WriteLine($"Project folder {projectFolder} is a file. Please supply location of project with --folder [path/to/project]"); + returnCode = 1; + return; + } + + if (!Path.IsPathRooted(projectFolder)) + { + projectFile = Path.Combine(Directory.GetCurrentDirectory(), projectFolder, projectFile); + processFile = Path.Combine(Directory.GetCurrentDirectory(), projectFolder, processFile); + appSettingsFolder = Path.Combine(Directory.GetCurrentDirectory(), projectFolder, appSettingsFolder); + } + else + { + projectFile = Path.Combine(projectFolder, projectFile); + processFile = Path.Combine(projectFolder, processFile); + appSettingsFolder = Path.Combine(projectFolder, appSettingsFolder); + } + + var projectChecks = new ProjectChecks(projectFile); + if (!projectChecks.SupportedSourceVersion()) + { + Console.WriteLine($"Version(s) in project file {projectFile} is not supported. Please upgrade to version 7.0.0 or higher."); + returnCode = 2; + return; + } + + if (!skipCodeUpgrade) + { + returnCode = await UpgradeCode(projectFile); + } + + if (!skipCsprojUpgrade && returnCode == 0) + { + returnCode = await UpgradeProjectFile(projectFile, targetVersion, targetFramework); + } + + if (!skipDockerUpgrade && returnCode == 0) + { + returnCode = await UpgradeDockerfile(Path.Combine(projectFolder, "Dockerfile"), targetFramework); + } + + if (!skipProcessUpgrade && returnCode == 0) + { + returnCode = await UpgradeProcess(processFile); + } + + if (!skipAppSettingsUpgrade && returnCode == 0) + { + returnCode = await UpgradeAppSettings(appSettingsFolder); + } + + if (returnCode == 0) + { + Console.WriteLine("Upgrade completed without errors. Please verify that the application is still working as expected."); + } + else + { + Console.WriteLine("Upgrade completed with errors. Please check for errors in the log above."); + } + } + ); + + versionCommand.SetHandler(() => + { + var version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion ?? "Unknown"; + Console.WriteLine($"altinn-app-cli v{version}"); + }); + await rootCommand.InvokeAsync(args); + return returnCode; + } + + static async Task UpgradeProjectFile(string projectFile, string targetVersion, string targetFramework) + { + if (!File.Exists(projectFile)) + { + Console.WriteLine($"Project file {projectFile} does not exist. Please supply location of project with --project [path/to/project.csproj]"); + return 1; + } + + Console.WriteLine("Trying to upgrade nuget versions in project file"); + var rewriter = new ProjectFileRewriter(projectFile, targetVersion, targetFramework); + await rewriter.Upgrade(); + Console.WriteLine("Nuget versions upgraded"); + return 0; + } + + static async Task UpgradeDockerfile(string dockerFile, string targetFramework) + { + if (!File.Exists(dockerFile)) + { + Console.WriteLine($"Dockerfile {dockerFile} does not exist. Please supply location of project with --dockerfile [path/to/Dockerfile]"); + return 1; + } + Console.WriteLine("Trying to upgrade dockerfile"); + var rewriter = new DockerfileRewriter(dockerFile, targetFramework); + await rewriter.Upgrade(); + Console.WriteLine("Dockerfile upgraded"); + return 0; + } + + static async Task UpgradeCode(string projectFile) + { + if (!File.Exists(projectFile)) + { + Console.WriteLine($"Project file {projectFile} does not exist. Please supply location of project with --project [path/to/project.csproj]"); + return 1; + } + + Console.WriteLine("Trying to upgrade references and using in code"); + + MSBuildLocator.RegisterDefaults(); + var workspace = MSBuildWorkspace.Create(); + var project = await workspace.OpenProjectAsync(projectFile); + var comp = await project.GetCompilationAsync(); + if (comp == null) + { + Console.WriteLine("Could not get compilation"); + return 1; + } + foreach (var sourceTree in comp.SyntaxTrees) + { + SemanticModel sm = comp.GetSemanticModel(sourceTree); + TypesRewriter rewriter = new(sm); + SyntaxNode newSource = rewriter.Visit(await sourceTree.GetRootAsync()); + if (newSource != await sourceTree.GetRootAsync()) + { + await File.WriteAllTextAsync(sourceTree.FilePath, newSource.ToFullString()); + } + + UsingRewriter usingRewriter = new(); + var newUsingSource = usingRewriter.Visit(newSource); + if (newUsingSource != newSource) + { + await File.WriteAllTextAsync(sourceTree.FilePath, newUsingSource.ToFullString()); + } + + DataProcessorRewriter dataProcessorRewriter = new(sm); + var dataProcessorSource = dataProcessorRewriter.Visit(newUsingSource); + if (dataProcessorSource != newUsingSource) + { + await File.WriteAllTextAsync(sourceTree.FilePath, dataProcessorSource.ToFullString()); + } + } + + Console.WriteLine("References and using upgraded"); + return 0; + } + + static async Task UpgradeProcess(string processFile) + { + if (!File.Exists(processFile)) + { + Console.WriteLine($"Process file {processFile} does not exist. Please supply location of project with --process [path/to/project.csproj]"); + return 1; + } + + Console.WriteLine("Trying to upgrade process file"); + ProcessUpgrader parser = new(processFile); + parser.Upgrade(); + await parser.Write(); + var warnings = parser.GetWarnings(); + foreach (var warning in warnings) + { + Console.WriteLine(warning); + } + + Console.WriteLine(warnings.Any() ? "Process file upgraded with warnings. Review the warnings above and make sure that the process file is still valid." : "Process file upgraded"); + + return 0; + } + + static async Task UpgradeAppSettings(string appSettingsFolder) + { + if (!Directory.Exists(appSettingsFolder)) + { + Console.WriteLine($"App settings folder {appSettingsFolder} does not exist. Please supply location with --appsettings-folder [path/to/appsettings]"); + return 1; + } + + if (Directory.GetFiles(appSettingsFolder, AppSettingsRewriter.APP_SETTINGS_FILE_PATTERN).Count() == 0) + { + Console.WriteLine($"No appsettings*.json files found in {appSettingsFolder}"); + return 1; + } + + Console.WriteLine("Trying to upgrade appsettings*.json files"); + AppSettingsRewriter rewriter = new(appSettingsFolder); + rewriter.Upgrade(); + await rewriter.Write(); + var warnings = rewriter.GetWarnings(); + foreach (var warning in warnings) + { + Console.WriteLine(warning); + } + + Console.WriteLine(warnings.Any() ? "AppSettings files upgraded with warnings. Review the warnings above and make sure that the appsettings files are still valid." : "AppSettings files upgraded"); + + return 0; + } +} diff --git a/cli-tools/altinn-app-cli/altinn-app-cli.csproj b/cli-tools/altinn-app-cli/altinn-app-cli.csproj new file mode 100644 index 000000000..0b29a106f --- /dev/null +++ b/cli-tools/altinn-app-cli/altinn-app-cli.csproj @@ -0,0 +1,42 @@ + + + + Exe + net8.0 + altinn_app_cli + latest + enable + enable + win-x64;linux-x64;osx-x64 + osx-x64 + altinn-app-cli + true + false + true + + + + + + + + + + + + + + + $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) + preview.0 + altinn-app-cli + true + 12.0 + + + + + $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) + + + diff --git a/cli-tools/altinn-app-cli/v7Tov8/AppSettingsRewriter/AppSettingsRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/AppSettingsRewriter/AppSettingsRewriter.cs new file mode 100644 index 000000000..860d2b1b3 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/AppSettingsRewriter/AppSettingsRewriter.cs @@ -0,0 +1,110 @@ + +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace altinn_app_cli.v7Tov8.AppSettingsRewriter; + + +/// +/// Rewrites the appsettings.*.json files +/// +public class AppSettingsRewriter +{ + /// + /// The pattern used to search for appsettings.*.json files + /// + public static readonly string APP_SETTINGS_FILE_PATTERN = "appsettings*.json"; + + private Dictionary appSettingsJsonCollection; + + private readonly IList warnings = new List(); + + /// + /// Initializes a new instance of the class. + /// + public AppSettingsRewriter(string appSettingsFolder) + { + appSettingsJsonCollection = new Dictionary(); + foreach (var file in Directory.GetFiles(appSettingsFolder, APP_SETTINGS_FILE_PATTERN)) + { + var json = File.ReadAllText(file); + var appSettingsJson = JsonNode.Parse(json); + if (appSettingsJson is not JsonObject appSettingsJsonObject) + { + warnings.Add($"Unable to parse AppSettings file {file} as a json object, skipping"); + continue; + } + + this.appSettingsJsonCollection.Add(file, appSettingsJsonObject); + } + } + + /// + /// Gets the warnings + /// + public IList GetWarnings() + { + return warnings; + } + + /// + /// Upgrades the appsettings.*.json files + /// + public void Upgrade() + { + foreach ((var fileName, var appSettingsJson) in appSettingsJsonCollection) + { + RewriteRemoveHiddenDataSetting(fileName, appSettingsJson); + } + } + + /// + /// Writes the appsettings.*.json files + /// + public async Task Write() + { + var tasks = appSettingsJsonCollection.Select(async appSettingsFiles => + { + appSettingsFiles.Deconstruct(out var fileName, out var appSettingsJson); + + JsonSerializerOptions options = new JsonSerializerOptions + { + WriteIndented = true, + }; + await File.WriteAllTextAsync(fileName, appSettingsJson.ToJsonString(options)); + }); + + await Task.WhenAll(tasks); + } + + private void RewriteRemoveHiddenDataSetting(string fileName, JsonObject settings) + { + // Look for "AppSettings" object + settings.TryGetPropertyValue("AppSettings", out var appSettingsNode); + if (appSettingsNode is not JsonObject appSettingsObject) + { + // No "AppSettings" object found, nothing to change + return; + } + + // Look for "RemoveHiddenDataPreview" property + appSettingsObject.TryGetPropertyValue("RemoveHiddenDataPreview", out var removeHiddenDataPreviewNode); + if (removeHiddenDataPreviewNode is not JsonValue removeHiddenDataPreviewValue) + { + // No "RemoveHiddenDataPreview" property found, nothing to change + return; + } + + // Get value of "RemoveHiddenDataPreview" property + if (!removeHiddenDataPreviewValue.TryGetValue(out var removeHiddenDataValue)) + { + warnings.Add($"RemoveHiddenDataPreview has unexpected value {removeHiddenDataPreviewValue.ToJsonString()} in {fileName}, expected a boolean"); + return; + } + + appSettingsObject.Remove("RemoveHiddenDataPreview"); + appSettingsObject.Add("RemoveHiddenData", removeHiddenDataValue); + appSettingsObject.Add("RequiredValidation", removeHiddenDataValue); + + } +} diff --git a/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/IDataProcessorRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/IDataProcessorRewriter.cs new file mode 100644 index 000000000..5b1081666 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/IDataProcessorRewriter.cs @@ -0,0 +1,179 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace altinn_app_cli.v7Tov8.CodeRewriters +{ + public class DataProcessorRewriter : CSharpSyntaxRewriter + { + private readonly SemanticModel semanticModel; + + public DataProcessorRewriter(SemanticModel semanticModel) + { + this.semanticModel = semanticModel; + } + + public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) + { + // Ignore any classes that don't implement `IDataProcessor` (consider using semantic model to ensure correct reference) + if (node.BaseList?.Types.Any(t => t.Type.ToString() == "IDataProcessor") == true) + { + var processDataWrite = node.Members.OfType() + .FirstOrDefault(m => m.Identifier.ValueText == "ProcessDataWrite"); + if (processDataWrite is not null) + { + node = node.ReplaceNode(processDataWrite, Update_DataProcessWrite(processDataWrite)); + } + + var processDataRead = node.Members.OfType() + .FirstOrDefault(m => m.Identifier.ValueText == "ProcessDataRead"); + if (processDataRead is not null) + { + node = node.ReplaceNode(processDataRead, Update_DataProcessRead(processDataRead)); + } + } + + return base.VisitClassDeclaration(node); + } + + private MethodDeclarationSyntax Update_DataProcessRead(MethodDeclarationSyntax processDataRead) + { + if (processDataRead.ParameterList.Parameters.Count == 3 && + processDataRead.ReturnType.ToString() == "Task") + { + processDataRead = ChangeReturnType_FromTaskBool_ToTask(processDataRead); + } + + return processDataRead; + } + + private MethodDeclarationSyntax Update_DataProcessWrite(MethodDeclarationSyntax processDataWrite) + { + if (processDataWrite.ParameterList.Parameters.Count == 3 && + processDataWrite.ReturnType.ToString() == "Task") + { + processDataWrite = AddParameter_ChangedFields(processDataWrite); + processDataWrite = ChangeReturnType_FromTaskBool_ToTask(processDataWrite); + } + + return processDataWrite; + } + + private MethodDeclarationSyntax AddParameter_ChangedFields(MethodDeclarationSyntax method) + { + return method.ReplaceNode(method.ParameterList, + method.ParameterList.AddParameters(SyntaxFactory.Parameter(SyntaxFactory.Identifier("previousData")) + .WithLeadingTrivia(SyntaxFactory.Space) + .WithType(SyntaxFactory.ParseTypeName("object?")) + .WithLeadingTrivia(SyntaxFactory.Space))); + } + + private MethodDeclarationSyntax ChangeReturnType_FromTaskBool_ToTask(MethodDeclarationSyntax method) + { + if (method.ReturnType.ToString() == "Task") + { + var returnTypeRewriter = new ReturnTypeTaskBooleanRewriter(); + method = (MethodDeclarationSyntax)returnTypeRewriter.Visit(method)!; + } + + return method; + + } + } + + public class ReturnTypeTaskBooleanRewriter : CSharpSyntaxRewriter + { + public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) + { + if (node.ReturnType.ToString() == "Task") + { + // Change return type + node = node.WithReturnType( + SyntaxFactory.ParseTypeName("Task").WithTrailingTrivia(SyntaxFactory.Space)); + } + return base.VisitMethodDeclaration(node); + } + + public override SyntaxNode? VisitBlock(BlockSyntax node) + { + foreach (var returnStatementSyntax in node.Statements.OfType()) + { + var leadingTrivia = returnStatementSyntax.GetLeadingTrivia(); + var trailingTrivia = returnStatementSyntax.GetTrailingTrivia(); + // When we add multiple lines of code, we need the indentation and a newline + var leadingTriviaMiddle = leadingTrivia.LastOrDefault(t => t.IsKind(SyntaxKind.WhitespaceTrivia)); + var trailingTriviaMiddle = trailingTrivia.FirstOrDefault(t => t.IsKind(SyntaxKind.EndOfLineTrivia)); + // If we don't find a newline, just guess that LF is used. Will likely work anyway. + if (trailingTriviaMiddle == default) trailingTriviaMiddle = SyntaxFactory.LineFeed; + + + switch (returnStatementSyntax.Expression) + { + // return true/false/variableName + case IdentifierNameSyntax: + case LiteralExpressionSyntax: + case null: + node = node.ReplaceNode(returnStatementSyntax, + SyntaxFactory.ReturnStatement() + .WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTrivia)); + break; + // case "Task.FromResult(...)": + case InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Expression: IdentifierNameSyntax { Identifier: {Text: "Task" } }, + Name: { Identifier: {Text: "FromResult"}} + }, + ArgumentList: { Arguments: { Count: 1 } } + }: + node = node.ReplaceNode(returnStatementSyntax, + SyntaxFactory.ReturnStatement(SyntaxFactory.ParseExpression(" Task.CompletedTask")) + .WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTrivia)); + break; + // case "await Task.FromResult(...)": + // Assume we need an await to silence CS1998 and rewrite to + // await Task.CompletedTask; return; + // Could be dropped if we ignore CS1998 + case AwaitExpressionSyntax + { + Expression: InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Expression: IdentifierNameSyntax { Identifier: {Text: "Task" } }, + Name: { Identifier: {Text: "FromResult"}} + }, + ArgumentList: { Arguments: [{Expression: IdentifierNameSyntax or LiteralExpressionSyntax}]} + } + }: + node = node.WithStatements(node.Statements.ReplaceRange(returnStatementSyntax, new StatementSyntax[] + { + // Uncomment if cs1998 isn't disabled + // SyntaxFactory.ParseStatement("await Task.CompletedTask;") + // .WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTriviaMiddle), + + SyntaxFactory.ReturnStatement() + .WithLeadingTrivia(leadingTriviaMiddle).WithTrailingTrivia(trailingTrivia), + + })); + break; + // Just add move the return; statement after the existing return value + default: + node = node.WithStatements(node.Statements.ReplaceRange(returnStatementSyntax, + new StatementSyntax[] + { + SyntaxFactory.ExpressionStatement(returnStatementSyntax.Expression) + .WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTriviaMiddle), + + SyntaxFactory.ReturnStatement() + .WithLeadingTrivia(leadingTriviaMiddle).WithTrailingTrivia(trailingTrivia), + })); + break; + } + } + + return base.VisitBlock(node); + } + } +} \ No newline at end of file diff --git a/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/TypesRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/TypesRewriter.cs new file mode 100644 index 000000000..8e7836248 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/TypesRewriter.cs @@ -0,0 +1,152 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace altinn_app_cli.v7Tov8.CodeRewriters; + +public class TypesRewriter: CSharpSyntaxRewriter +{ + private readonly SemanticModel semanticModel; + private readonly Dictionary fieldDescendantsMapping = new Dictionary() + { + {"Altinn.App.Core.Interface.IAppEvents", SyntaxFactory.IdentifierName("IAppEvents")}, + {"Altinn.App.Core.Interface.IApplication", SyntaxFactory.IdentifierName("IApplicationClient")}, + {"Altinn.App.Core.Interface.IAppResources", SyntaxFactory.IdentifierName("IAppResources")}, + {"Altinn.App.Core.Interface.IAuthentication", SyntaxFactory.IdentifierName("IAuthenticationClient")}, + {"Altinn.App.Core.Interface.IAuthorization", SyntaxFactory.IdentifierName("IAuthorizationClient")}, + {"Altinn.App.Core.Interface.IData", SyntaxFactory.IdentifierName("IDataClient")}, + {"Altinn.App.Core.Interface.IDSF", SyntaxFactory.IdentifierName("IPersonClient")}, + {"Altinn.App.Core.Interface.IER", SyntaxFactory.IdentifierName("IOrganizationClient")}, + {"Altinn.App.Core.Interface.IEvents", SyntaxFactory.IdentifierName("IEventsClient")}, + {"Altinn.App.Core.Interface.IInstance", SyntaxFactory.IdentifierName("IInstanceClient")}, + {"Altinn.App.Core.Interface.IInstanceEvent", SyntaxFactory.IdentifierName("IInstanceEventClient")}, + {"Altinn.App.Core.Interface.IPersonLookup", SyntaxFactory.IdentifierName("IPersonClient")}, + {"Altinn.App.Core.Interface.IPersonRetriever", SyntaxFactory.IdentifierName("IPersonClient")}, + {"Altinn.App.Core.Interface.IPrefill", SyntaxFactory.IdentifierName("IPrefill")}, + {"Altinn.App.Core.Interface.IProcess", SyntaxFactory.IdentifierName("IProcessClient")}, + {"Altinn.App.Core.Interface.IProfile", SyntaxFactory.IdentifierName("IProfileClient")}, + {"Altinn.App.Core.Interface.IRegister", SyntaxFactory.IdentifierName("IAltinnPartyClient")}, + {"Altinn.App.Core.Interface.ISecrets", SyntaxFactory.IdentifierName("ISecretsClient")}, + {"Altinn.App.Core.Interface.ITaskEvents", SyntaxFactory.IdentifierName("ITaskEvents")}, + {"Altinn.App.Core.Interface.IUserTokenProvider", SyntaxFactory.IdentifierName("IUserTokenProvider")} + }; + private readonly IEnumerable statementsToRemove = new List() + { + "app.UseDefaultSecurityHeaders();", + "app.UseRouting();", + "app.UseStaticFiles('/' + applicationId);", + "app.UseAuthentication();", + "app.UseAuthorization();", + "app.UseEndpoints(endpoints", + "app.UseHealthChecks(\"/health\");", + "app.UseAltinnAppCommonConfiguration();" + }; + + public TypesRewriter(SemanticModel semanticModel) + { + this.semanticModel = semanticModel; + } + + public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node) + { + return UpdateField(node); + } + + public override SyntaxNode? VisitParameter(ParameterSyntax node) + { + var parameterTypeName = node.Type; + if(parameterTypeName is null) + { + return node; + } + var parameterType = (ITypeSymbol?)semanticModel.GetSymbolInfo(parameterTypeName).Symbol; + if(parameterType?.ToString() != null && fieldDescendantsMapping.TryGetValue(parameterType.ToString()!, out var newType)) + { + var newTypeName = newType.WithLeadingTrivia(parameterTypeName.GetLeadingTrivia()).WithTrailingTrivia(parameterTypeName.GetTrailingTrivia()); + return node.ReplaceNode(parameterTypeName, newTypeName); + } + + return node; + } + + public override SyntaxNode? VisitGlobalStatement(GlobalStatementSyntax node) + { + if (node.Statement is LocalFunctionStatementSyntax localFunctionStatementSyntax) + { + if(localFunctionStatementSyntax.Identifier.Text == "Configure" && !localFunctionStatementSyntax.ParameterList.Parameters.Any() && localFunctionStatementSyntax.Body != null) + { + SyntaxTriviaList leadingTrivia = SyntaxFactory.TriviaList(); + SyntaxTriviaList trailingTrivia = SyntaxFactory.TriviaList(); + var newBody = SyntaxFactory.Block().WithoutLeadingTrivia().WithTrailingTrivia(localFunctionStatementSyntax.Body.GetTrailingTrivia()); + foreach (var childNode in localFunctionStatementSyntax.Body.ChildNodes()) + { + if(childNode is IfStatementSyntax ifStatementSyntax && ifStatementSyntax.Condition.ToString()!="app.Environment.IsDevelopment()") + { + newBody = AddStatementWithTrivia(newBody, ifStatementSyntax); + } + if(childNode is ExpressionStatementSyntax statementSyntax){ + leadingTrivia = statementSyntax.GetLeadingTrivia(); + trailingTrivia = statementSyntax.GetTrailingTrivia(); + if (!ShouldRemoveStatement(statementSyntax)) + { + newBody = AddStatementWithTrivia(newBody, statementSyntax); + } + } + if(childNode is LocalDeclarationStatementSyntax localDeclarationStatement) + { + newBody = AddStatementWithTrivia(newBody, localDeclarationStatement); + } + } + newBody = newBody.AddStatements(SyntaxFactory.ParseStatement("app.UseAltinnAppCommonConfiguration();").WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTrivia)); + return node.ReplaceNode(localFunctionStatementSyntax.Body, newBody); + } + } + + return node; + } + + public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node) + { + if (node.Identifier.Text == "FilterAsync" && + node.Parent is ClassDeclarationSyntax { BaseList: not null } classDeclarationSyntax + && classDeclarationSyntax.BaseList.Types.Any(x => x.Type.ToString() == "IProcessExclusiveGateway") + && node.ParameterList.Parameters.All(x => x.Type?.ToString() != "ProcessGatewayInformation")) + { + return node.AddParameterListParameters( + SyntaxFactory.Parameter(SyntaxFactory.Identifier("processGatewayInformation").WithLeadingTrivia(SyntaxFactory.ElasticSpace)).WithType(SyntaxFactory.ParseTypeName("ProcessGatewayInformation").WithLeadingTrivia(SyntaxFactory.ElasticSpace)) + ); + } + + return node; + } + + private FieldDeclarationSyntax UpdateField(FieldDeclarationSyntax node) + { + var variableTypeName = node.Declaration.Type; + var variableType = (ITypeSymbol?)semanticModel.GetSymbolInfo(variableTypeName).Symbol; + if(variableType?.ToString() != null && fieldDescendantsMapping.TryGetValue(variableType.ToString()!, out var newType)) + { + var newTypeName = newType.WithLeadingTrivia(variableTypeName.GetLeadingTrivia()).WithTrailingTrivia(variableTypeName.GetTrailingTrivia()); + node = node.ReplaceNode(variableTypeName, newTypeName); + Console.WriteLine($"Updated field {node.Declaration.Variables.First().Identifier.Text} from {variableType} to {newType}"); + } + return node; + } + + private bool ShouldRemoveStatement(StatementSyntax statementSyntax) + { + foreach (var statementToRemove in statementsToRemove) + { + var s = statementSyntax.ToString(); + if(s == statementToRemove || s.StartsWith(statementToRemove)) + { + return true; + } + } + return false; + } + private static BlockSyntax AddStatementWithTrivia(BlockSyntax block, StatementSyntax statement) + { + return block.AddStatements(statement).WithLeadingTrivia(statement.GetLeadingTrivia()).WithTrailingTrivia(statement.GetTrailingTrivia()); + } +} diff --git a/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/UsingRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/UsingRewriter.cs new file mode 100644 index 000000000..1d692a3a7 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/UsingRewriter.cs @@ -0,0 +1,91 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace altinn_app_cli.v7Tov8.CodeRewriters; + +public class UsingRewriter: CSharpSyntaxRewriter +{ + private const string CommonInterfaceNamespace = "Altinn.App.Core.Interface"; + + private readonly Dictionary usingMappings = new Dictionary() + { + {"IAppEvents", "Altinn.App.Core.Internal.App"}, + {"IApplication", "Altinn.App.Core.Internal.App"}, + {"IAppResources", "Altinn.App.Core.Internal.App"}, + {"IAuthenticationClient", "Altinn.App.Core.Internal.Auth"}, + {"IAuthorizationClient", "Altinn.App.Core.Internal.Auth"}, + {"IDataClient", "Altinn.App.Core.Internal.Data"}, + {"IPersonClient", "Altinn.App.Core.Internal.Registers"}, + {"IOrganizationClient", "Altinn.App.Core.Internal.Registers"}, + {"IEventsClient", "Altinn.App.Core.Internal.Events"}, + {"IInstanceClient", "Altinn.App.Core.Internal.Instances"}, + {"IInstanceEventClient", "Altinn.App.Core.Internal.Instances"}, + {"IPrefill", "Altinn.App.Core.Internal.Prefill"}, + {"IProcessClient", "Altinn.App.Core.Internal.Process"}, + {"IProfileClient", "Altinn.App.Core.Internal.Profile"}, + {"IAltinnPartyClient", "Altinn.App.Core.Internal.Registers"}, + {"ISecretsClient", "Altinn.App.Core.Internal.Secrets"}, + {"ITaskEvents", "Altinn.App.Core.Internal.Process"}, + {"IUserTokenProvider", "Altinn.App.Core.Internal.Auth"}, + }; + + public override SyntaxNode? VisitCompilationUnit(CompilationUnitSyntax node) + { + foreach (var mapping in usingMappings) + { + if (HasFieldOfType(node, mapping.Key)) + { + node = AddUsing(node, mapping.Value); + } + } + + if (ImplementsIProcessExclusiveGateway(node)) + { + node = AddUsing(node, "Altinn.App.Core.Models.Process"); + } + + return RemoveOldUsing(node); + } + + private bool HasFieldOfType(CompilationUnitSyntax node, string typeName) + { + var fieldDecendants = node.DescendantNodes().OfType(); + return fieldDecendants.Any(f => f.Declaration.Type.ToString() == typeName); + } + + private bool ImplementsIProcessExclusiveGateway(CompilationUnitSyntax node) + { + var classDecendants = node.DescendantNodes().OfType(); + return classDecendants.Any(c => c.BaseList?.Types.Any(t => t.Type.ToString() == "IProcessExclusiveGateway") == true); + } + + private CompilationUnitSyntax AddUsing(CompilationUnitSyntax node, string usingString) + { + if (HasUsingDefined(node, usingString)) + { + return node; + } + var usingName = SyntaxFactory.ParseName(usingString); + var usingDirective = SyntaxFactory.UsingDirective(usingName).NormalizeWhitespace().WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed); + return node.AddUsings(usingDirective); + } + + private bool HasUsingDefined(CompilationUnitSyntax node, string usingName) + { + var usingDirectiveSyntaxes = node.DescendantNodes().OfType(); + return usingDirectiveSyntaxes.Any(u => u.Name?.ToString() == usingName); + } + + private CompilationUnitSyntax? RemoveOldUsing(CompilationUnitSyntax node) + { + var usingDirectiveSyntaxes = node.DescendantNodes().OfType(); + var usingDirectiveSyntax = usingDirectiveSyntaxes.FirstOrDefault(u => u.Name?.ToString() == CommonInterfaceNamespace); + if (usingDirectiveSyntax != null) + { + return node.RemoveNode(usingDirectiveSyntax, SyntaxRemoveOptions.KeepNoTrivia); + } + return node; + } + +} diff --git a/cli-tools/altinn-app-cli/v7Tov8/DockerfileRewriters/DockerfileRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/DockerfileRewriters/DockerfileRewriter.cs new file mode 100644 index 000000000..6316d3f5a --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/DockerfileRewriters/DockerfileRewriter.cs @@ -0,0 +1,50 @@ +using System.Text.RegularExpressions; +using altinn_app_cli.v7Tov8.DockerfileRewriters.Extensions; + +namespace altinn_app_cli.v7Tov8.DockerfileRewriters; + +/// +/// Rewrites the dockerfile +/// +public class DockerfileRewriter +{ + private readonly string dockerFilePath; + private readonly string targetFramework; + + /// + /// Creates a new instance of the class + /// + /// + /// + public DockerfileRewriter(string dockerFilePath, string targetFramework = "net8.0") + { + this.dockerFilePath = dockerFilePath; + this.targetFramework = targetFramework; + } + + /// + /// Upgrades the dockerfile + /// + public async Task Upgrade() + { + var dockerFile = await File.ReadAllLinesAsync(dockerFilePath); + var newDockerFile = new List(); + foreach (var line in dockerFile) + { + var imageTag = GetImageTagFromFrameworkVersion(targetFramework); + newDockerFile.Add(line.ReplaceSdkVersion(imageTag).ReplaceAspNetVersion(imageTag)); + } + + await File.WriteAllLinesAsync(dockerFilePath, newDockerFile); + } + + private static string GetImageTagFromFrameworkVersion(string targetFramework) + { + return targetFramework switch + { + "net6.0" => "6.0-alpine", + "net7.0" => "7.0-alpine", + _ => "8.0-alpine" + }; + } +} \ No newline at end of file diff --git a/cli-tools/altinn-app-cli/v7Tov8/DockerfileRewriters/Extensions/StringDockerTagExtensions.cs b/cli-tools/altinn-app-cli/v7Tov8/DockerfileRewriters/Extensions/StringDockerTagExtensions.cs new file mode 100644 index 000000000..8f62aea74 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/DockerfileRewriters/Extensions/StringDockerTagExtensions.cs @@ -0,0 +1,33 @@ +using System.Text.RegularExpressions; + +namespace altinn_app_cli.v7Tov8.DockerfileRewriters.Extensions; + +/// +/// Extensions for string replacing tags in dockerfiles +/// +public static class DockerfileStringExtensions +{ + /// + /// Replaces the dotnet sdk image tag version in a dockerfile + /// + /// a line in the dockerfile + /// the new image tag + /// + public static string ReplaceSdkVersion(this string line, string imageTag) + { + const string pattern = @"(^FROM mcr.microsoft.com/dotnet/sdk):(.+?)( AS .*)?$"; + return Regex.Replace(line, pattern, $"$1:{imageTag}$3"); + } + + /// + /// Replaces the aspnet image tag version in a dockerfile + /// + /// a line in the dockerfile + /// the new image tag + /// + public static string ReplaceAspNetVersion(this string line, string imageTag) + { + const string pattern = @"(^FROM mcr.microsoft.com/dotnet/aspnet):(.+?)( AS .*)?$"; + return Regex.Replace(line, pattern, $"$1:{imageTag}$3"); + } +} \ No newline at end of file diff --git a/cli-tools/altinn-app-cli/v7Tov8/ProcessRewriter/ProcessUpgrader.cs b/cli-tools/altinn-app-cli/v7Tov8/ProcessRewriter/ProcessUpgrader.cs new file mode 100644 index 000000000..e23be8615 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/ProcessRewriter/ProcessUpgrader.cs @@ -0,0 +1,168 @@ +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace altinn_app_cli.v7Tov8.ProcessRewriter; + +public class ProcessUpgrader +{ + private XDocument doc; + private readonly string processFile; + private readonly XNamespace newAltinnNs = "http://altinn.no/process"; + private readonly XNamespace origAltinnNs = "http://altinn.no"; + private readonly XNamespace bpmnNs = "http://www.omg.org/spec/BPMN/20100524/MODEL"; + private readonly IList warnings = new List(); + + public ProcessUpgrader(string processFile) + { + this.processFile = processFile; + var xmlString = File.ReadAllText(processFile); + xmlString = xmlString.Replace($"xmlns:altinn=\"{origAltinnNs}\"", $"xmlns:altinn=\"{newAltinnNs}\""); + doc = XDocument.Parse(xmlString); + } + + public void Upgrade() + { + var definitions = doc.Root; + var process = definitions?.Elements().Single(e => e.Name.LocalName == "process"); + var processElements = process?.Elements() ?? Enumerable.Empty(); + foreach (var processElement in processElements) + { + if (processElement.Name.LocalName == "task") + { + UpgradeTask(processElement); + } + else if (processElement.Name.LocalName == "sequenceFlow") + { + UpgradeSequenceFlow(processElement); + } + } + } + + private void UpgradeTask(XElement processElement) + { + var taskTypeAttr = processElement.Attribute(newAltinnNs + "tasktype"); + var taskType = taskTypeAttr?.Value; + if (taskType == null) + { + return; + } + XElement extensionElements = processElement.Element(bpmnNs + "extensionElements") ?? new XElement(bpmnNs + "extensionElements"); + XElement taskExtensionElement = extensionElements.Element(newAltinnNs + "taskExtension") ?? new XElement(newAltinnNs + "taskExtension"); + XElement taskTypeElement = new XElement(newAltinnNs + "taskType"); + taskTypeElement.Value = taskType; + taskExtensionElement.Add(taskTypeElement); + extensionElements.Add(taskExtensionElement); + processElement.Add(extensionElements); + taskTypeAttr?.Remove(); + if (taskType.Equals("confirmation")) + { + AddAction(processElement, "confirm"); + } + } + + private void UpgradeSequenceFlow(XElement processElement) + { + var flowTypeAttr = processElement.Attribute(newAltinnNs + "flowtype"); + flowTypeAttr?.Remove(); + if (flowTypeAttr?.Value != "AbandonCurrentReturnToNext") + { + return; + } + + var sourceRefAttr = processElement.Attribute("sourceRef"); + SetSequenceFlowAsDefaultIfGateway(sourceRefAttr?.Value!, processElement.Attribute("id")?.Value!); + var sourceTask = FollowGatewaysAndGetSourceTask(sourceRefAttr?.Value!); + AddAction(sourceTask, "reject"); + var conditionExpression = processElement.Elements().FirstOrDefault(e => e.Name.LocalName == "conditionExpression"); + if(conditionExpression == null) + { + conditionExpression = new XElement(bpmnNs + "conditionExpression"); + processElement.Add(conditionExpression); + } + conditionExpression.Value = "[\"equals\", [\"gatewayAction\"],\"reject\"]"; + warnings.Add($"SequenceFlow {processElement.Attribute("id")?.Value!} has flowtype {flowTypeAttr.Value} upgrade tool has tried to add reject action to source task. \nPlease verify that process flow is correct and that layoutfiels are updated to use ActionButtons\nRefere to docs.altinn.studio for how actions in v8 work"); + } + + private void SetSequenceFlowAsDefaultIfGateway(string elementRef, string sequenceFlowRef) + { + var sourceElement = doc.Root?.Elements().Single(e => e.Name.LocalName == "process").Elements().Single(e => e.Attribute("id")?.Value == elementRef); + if (sourceElement?.Name.LocalName == "exclusiveGateway") + { + if (sourceElement.Attribute("default") == null) + { + sourceElement.Add(new XAttribute("default", sequenceFlowRef)); + } + else + { + warnings.Add($"Default sequence flow already set for gateway {elementRef}. Process is most likely not correct. Please correct it manually and test it."); + } + } + } + + private XElement FollowGatewaysAndGetSourceTask(string sourceRef) + { + var processElement = doc.Root?.Elements().Single(e => e.Name.LocalName == "process"); + var sourceElement = processElement?.Elements().Single(e => e.Attribute("id")?.Value == sourceRef); + if (sourceElement?.Name.LocalName == "task") + { + return sourceElement; + } + + if (sourceElement?.Name.LocalName == "exclusiveGateway") + { + var incomingSequenceFlow = sourceElement.Elements().Single(e => e.Name.LocalName == "incoming").Value; + var incomingSequenceFlowRef = processElement?.Elements().Single(e => e.Attribute("id")!.Value == incomingSequenceFlow).Attribute("sourceRef")?.Value; + return FollowGatewaysAndGetSourceTask(incomingSequenceFlowRef!); + } + + throw new Exception("Unexpected element type"); + } + + private void AddAction(XElement sourceTask, string actionName) + { + var extensionElements = sourceTask.Element(bpmnNs + "extensionElements"); + if (extensionElements == null) + { + extensionElements = new XElement(bpmnNs + "extensionElements"); + sourceTask.Add(extensionElements); + } + + var taskExtensionElement = extensionElements.Element(newAltinnNs + "taskExtension"); + if (taskExtensionElement == null) + { + taskExtensionElement = new XElement(newAltinnNs + "taskExtension"); + extensionElements.Add(taskExtensionElement); + } + + var actions = taskExtensionElement.Element(newAltinnNs + "actions"); + if (actions == null) + { + actions = new XElement(newAltinnNs + "actions"); + taskExtensionElement.Add(actions); + } + if(actions.Elements().Any(e => e.Value == actionName)) + { + return; + } + var action = new XElement(newAltinnNs + "action"); + action.Value = actionName; + actions.Add(action); + } + + public async Task Write() + { + XmlWriterSettings xws = new XmlWriterSettings(); + xws.Async = true; + xws.OmitXmlDeclaration = false; + xws.Indent = true; + xws.Encoding = Encoding.UTF8; + await using XmlWriter xw = XmlWriter.Create(processFile, xws); + await doc.WriteToAsync(xw, CancellationToken.None); + } + + public IList GetWarnings() + { + return warnings; + } +} diff --git a/cli-tools/altinn-app-cli/v7Tov8/ProjectChecks/ProjectChecks.cs b/cli-tools/altinn-app-cli/v7Tov8/ProjectChecks/ProjectChecks.cs new file mode 100644 index 000000000..4c4ddc73b --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/ProjectChecks/ProjectChecks.cs @@ -0,0 +1,71 @@ +using System.Xml.Linq; + +namespace altinn_app_cli.v7Tov8.ProjectChecks; + +public class ProjectChecks +{ + private XDocument doc; + + public ProjectChecks(string projectFilePath) + { + var xmlString = File.ReadAllText(projectFilePath); + doc = XDocument.Parse(xmlString); + } + + public bool SupportedSourceVersion() + { + var altinnAppCoreElements = GetAltinnAppCoreElement(); + var altinnAppApiElements = GetAltinnAppApiElement(); + if (altinnAppCoreElements == null || altinnAppApiElements == null) + { + return false; + } + + if (altinnAppApiElements.Select(apiElement => apiElement.Attribute("Version")?.Value).Any(altinnAppApiVersion => !SupportedSourceVersion(altinnAppApiVersion))) + { + return false; + } + + return altinnAppCoreElements.Select(coreElement => coreElement.Attribute("Version")?.Value).All(altinnAppCoreVersion => SupportedSourceVersion(altinnAppCoreVersion)); + + } + + private List? GetAltinnAppCoreElement() + { + return doc.Root?.Elements("ItemGroup").Elements("PackageReference").Where(x => x.Attribute("Include")?.Value == "Altinn.App.Core").ToList(); + } + + private List? GetAltinnAppApiElement() + { + return doc.Root?.Elements("ItemGroup").Elements("PackageReference").Where(x => x.Attribute("Include")?.Value == "Altinn.App.Api").ToList(); + } + + /// + /// Check that version is >=7.0.0 + /// + /// + /// + private bool SupportedSourceVersion(string? version) + { + if (version == null) + { + return false; + } + + var versionParts = version.Split('.'); + if (versionParts.Length < 3) + { + return false; + } + + if (int.TryParse(versionParts[0], out int major)) + { + if (major >= 7) + { + return true; + } + } + + return false; + } +} diff --git a/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs new file mode 100644 index 000000000..a34160194 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs @@ -0,0 +1,87 @@ +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace altinn_app_cli.v7Tov8.ProjectRewriters; + +public class ProjectFileRewriter +{ + private XDocument doc; + private readonly string projectFilePath; + private readonly string targetVersion; + private readonly string targetFramework; + + public ProjectFileRewriter(string projectFilePath, string targetVersion = "8.0.0", string targetFramework = "net8.0") + { + this.projectFilePath = projectFilePath; + this.targetVersion = targetVersion; + var xmlString = File.ReadAllText(projectFilePath); + doc = XDocument.Parse(xmlString); + this.targetFramework = targetFramework; + } + + public async Task Upgrade() + { + var altinnAppCoreElements = GetAltinnAppCoreElement(); + altinnAppCoreElements?.ForEach(c => c.Attribute("Version")?.SetValue(targetVersion)); + + var altinnAppApiElements = GetAltinnAppApiElement(); + altinnAppApiElements?.ForEach(a => a.Attribute("Version")?.SetValue(targetVersion)); + + IgnoreWarnings("1591", "1998"); // Require xml doc and await in async methods + + GetTargetFrameworkElement()?.ForEach(t => t.SetValue(targetFramework)); + + await Save(); + } + + private void IgnoreWarnings(params string[] warnings) + { + var noWarn = doc.Root?.Elements("PropertyGroup").Elements("NoWarn").ToList(); + switch (noWarn?.Count) + { + case 0: + doc.Root?.Elements("PropertyGroup").First().Add(new XElement("NoWarn", "$(NoWarn);" + string.Join(';', warnings))); + break; + + case 1: + var valueElement = noWarn.First(); + foreach (var warning in warnings) + { + if (!valueElement.Value.Contains(warning)) + { + valueElement.SetValue($"{valueElement.Value};{warning}"); + } + } + + break; + } + } + + private List? GetAltinnAppCoreElement() + { + return doc.Root?.Elements("ItemGroup").Elements("PackageReference").Where(x => x.Attribute("Include")?.Value == "Altinn.App.Core").ToList(); + } + + private List? GetAltinnAppApiElement() + { + return doc.Root?.Elements("ItemGroup").Elements("PackageReference").Where(x => x.Attribute("Include")?.Value == "Altinn.App.Api").ToList(); + } + + private List? GetTargetFrameworkElement() + { + return doc.Root?.Elements("PropertyGroup").Elements("TargetFramework").ToList(); + } + + private async Task Save() + { + XmlWriterSettings xws = new XmlWriterSettings(); + xws.Async = true; + xws.OmitXmlDeclaration = true; + xws.Indent = true; + xws.Encoding = Encoding.UTF8; + await using XmlWriter xw = XmlWriter.Create(projectFilePath, xws); + await doc.WriteToAsync(xw, CancellationToken.None); + await xw.FlushAsync(); + } +} diff --git a/src/Altinn.App.Api/Altinn.App.Api.csproj b/src/Altinn.App.Api/Altinn.App.Api.csproj index aa4699288..b6378ae28 100644 --- a/src/Altinn.App.Api/Altinn.App.Api.csproj +++ b/src/Altinn.App.Api/Altinn.App.Api.csproj @@ -1,7 +1,7 @@ - + - net6.0 + net8.0 Library Altinn.App.Api Altinn;Studio;App;Api;Controllers @@ -13,7 +13,7 @@ git https://github.com/Altinn/app-lib-dotnet true - true + enable {E8F29FE8-6B62-41F1-A08C-2A318DD08BB4} @@ -48,4 +48,7 @@ $(NoWarn);1591 + + + diff --git a/src/Altinn.App.Api/Controllers/ActionsController.cs b/src/Altinn.App.Api/Controllers/ActionsController.cs new file mode 100644 index 000000000..a83a4e888 --- /dev/null +++ b/src/Altinn.App.Api/Controllers/ActionsController.cs @@ -0,0 +1,129 @@ +#nullable enable +using Altinn.App.Api.Infrastructure.Filters; +using Altinn.App.Api.Models; +using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Internal.Exceptions; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.UserAction; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using IAuthorizationService = Altinn.App.Core.Internal.Auth.IAuthorizationService; + +namespace Altinn.App.Api.Controllers; + +/// +/// Controller that handles actions performed by users +/// +[AutoValidateAntiforgeryTokenIfAuthCookie] +[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] +[Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/actions")] +public class ActionsController : ControllerBase +{ + private readonly IAuthorizationService _authorization; + private readonly IInstanceClient _instanceClient; + private readonly UserActionService _userActionService; + + /// + /// Create new instance of the class + /// + /// The authorization service + /// The instance client + /// The user action service + public ActionsController(IAuthorizationService authorization, IInstanceClient instanceClient, UserActionService userActionService) + { + _authorization = authorization; + _instanceClient = instanceClient; + _userActionService = userActionService; + } + + /// + /// Perform a task action on an instance + /// + /// unique identfier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that this the owner of the instance + /// unique id to identify the instance + /// user action request + /// + [HttpPost] + [Authorize] + [ProducesResponseType(typeof(UserActionResponse), 200)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(401)] + public async Task> Perform( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromBody] UserActionRequest actionRequest) + { + var action = actionRequest.Action; + if (action == null) + { + return new BadRequestObjectResult(new ProblemDetails() + { + Instance = instanceGuid.ToString(), + Status = 400, + Title = "Action is missing", + Detail = "Action is missing in the request" + }); + } + + var instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + + if (instance?.Process == null) + { + return Conflict($"Process is not started."); + } + + if (instance.Process.Ended.HasValue) + { + return Conflict($"Process is ended."); + } + + var userId = HttpContext.User.GetUserIdAsInt(); + if (userId == null) + { + return Unauthorized(); + } + + var authorized = await _authorization.AuthorizeAction(new AppIdentifier(org, app), new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), HttpContext.User, action, instance.Process?.CurrentTask?.ElementId); + if (!authorized) + { + return Forbid(); + } + + UserActionContext userActionContext = new UserActionContext(instance, userId.Value, actionRequest.ButtonId, actionRequest.Metadata); + var actionHandler = _userActionService.GetActionHandler(action); + if (actionHandler == null) + { + return new NotFoundObjectResult(new UserActionResponse() + { + Error = new ActionError() + { + Code = "ActionNotFound", + Message = $"Action handler with id {action} not found", + } + }); + } + + var result = await actionHandler.HandleAction(userActionContext); + + if (!result.Success) + { + return new BadRequestObjectResult(new UserActionResponse() + { + ClientActions = result.ClientActions, + Error = result.Error + }); + } + + return new OkObjectResult(new UserActionResponse() + { + ClientActions = result.ClientActions, + UpdatedDataModels = result.UpdatedDataModels + }); + } +} diff --git a/src/Altinn.App.Api/Controllers/ApplicationMetadataController.cs b/src/Altinn.App.Api/Controllers/ApplicationMetadataController.cs index 2e7195deb..ffbfb710e 100644 --- a/src/Altinn.App.Api/Controllers/ApplicationMetadataController.cs +++ b/src/Altinn.App.Api/Controllers/ApplicationMetadataController.cs @@ -30,7 +30,7 @@ public ApplicationMetadataController(IAppMetadata appMetadata, ILogger - /// Get the application metadata + /// Get the application metadata https://altinncdn.no/schemas/json/application/application-metadata.schema.v1.json /// /// If org and app does not match, this returns a 409 Conflict response /// diff --git a/src/Altinn.App.Api/Controllers/AuthenticationController.cs b/src/Altinn.App.Api/Controllers/AuthenticationController.cs index 526ba7361..2749bde11 100644 --- a/src/Altinn.App.Api/Controllers/AuthenticationController.cs +++ b/src/Altinn.App.Api/Controllers/AuthenticationController.cs @@ -1,9 +1,7 @@ -using System.Threading.Tasks; using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Auth; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -14,13 +12,13 @@ namespace Altinn.App.Api.Controllers /// public class AuthenticationController : ControllerBase { - private readonly IAuthentication _authenticationClient; + private readonly IAuthenticationClient _authenticationClient; private readonly GeneralSettings _settings; /// /// Initializes a new instance of the class /// - public AuthenticationController(IAuthentication authenticationClient, IOptions settings) + public AuthenticationController(IAuthenticationClient authenticationClient, IOptions settings) { _authenticationClient = authenticationClient; _settings = settings.Value; diff --git a/src/Altinn.App.Api/Controllers/AuthorizationController.cs b/src/Altinn.App.Api/Controllers/AuthorizationController.cs index 56cdef38d..97530f181 100644 --- a/src/Altinn.App.Api/Controllers/AuthorizationController.cs +++ b/src/Altinn.App.Api/Controllers/AuthorizationController.cs @@ -1,10 +1,12 @@ -using System.Threading.Tasks; +#nullable enable + using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -15,7 +17,7 @@ namespace Altinn.App.Api.Controllers /// public class AuthorizationController : Controller { - private readonly IAuthorization _authorization; + private readonly IAuthorizationClient _authorization; private readonly UserHelper _userHelper; private readonly GeneralSettings _settings; @@ -23,12 +25,12 @@ public class AuthorizationController : Controller /// Initializes a new instance of the class /// public AuthorizationController( - IAuthorization authorization, - IProfile profileClient, - IRegister registerClient, + IAuthorizationClient authorization, + IProfileClient profileClient, + IAltinnPartyClient altinnPartyClientClient, IOptions settings) { - _userHelper = new UserHelper(profileClient, registerClient, settings); + _userHelper = new UserHelper(profileClient, altinnPartyClientClient, settings); _authorization = authorization; _settings = settings.Value; } @@ -71,7 +73,7 @@ public async Task GetCurrentParty(bool returnPartyObject = false) } } - string cookieValue = Request.Cookies[_settings.GetAltinnPartyCookieName]; + string? cookieValue = Request.Cookies[_settings.GetAltinnPartyCookieName]; if (!int.TryParse(cookieValue, out int partyIdFromCookie)) { partyIdFromCookie = 0; diff --git a/src/Altinn.App.Api/Controllers/DataController.cs b/src/Altinn.App.Api/Controllers/DataController.cs index 4f08e29f2..7353c8e10 100644 --- a/src/Altinn.App.Api/Controllers/DataController.cs +++ b/src/Altinn.App.Api/Controllers/DataController.cs @@ -1,7 +1,15 @@ +#nullable enable + +using System.Collections; using System.Net; +using System.Reflection; using System.Security.Claims; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using Altinn.App.Api.Helpers.RequestHandling; using Altinn.App.Api.Infrastructure.Filters; +using Altinn.App.Api.Models; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; @@ -10,12 +18,15 @@ using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; using Altinn.App.Core.Helpers.Serialization; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Prefill; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; +using Json.Patch; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; @@ -28,22 +39,31 @@ namespace Altinn.App.Api.Controllers /// The data controller handles creation, update, validation and calculation of data elements. /// [AutoValidateAntiforgeryTokenIfAuthCookie] + [ApiController] [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/data")] public class DataController : ControllerBase { private readonly ILogger _logger; - private readonly IData _dataClient; - private readonly IDataProcessor _dataProcessor; - private readonly IInstance _instanceClient; + private readonly IDataClient _dataClient; + private readonly IEnumerable _dataProcessors; + private readonly IInstanceClient _instanceClient; private readonly IInstantiationProcessor _instantiationProcessor; private readonly IAppModel _appModel; private readonly IAppResources _appResourcesService; private readonly IAppMetadata _appMetadata; private readonly IPrefill _prefillService; + private readonly IValidationService _validationService; private readonly IFileAnalysisService _fileAnalyserService; private readonly IFileValidationService _fileValidationService; private readonly IFeatureManager _featureManager; + + private static readonly JsonSerializerOptions JsonSerializerOptions = new() + { + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, + PropertyNameCaseInsensitive = true, + }; + private const long REQUEST_SIZE_LIMIT = 2000 * 1024 * 1024; /// @@ -53,23 +73,25 @@ public class DataController : ControllerBase /// instance service to store instances /// Instantiation processor /// A service with access to data storage. - /// Serive implemnting logic during data read/write + /// Services implementing logic during data read/write /// Service for generating app model /// The apps resource service /// The app metadata service /// The feature manager controlling enabled features. /// A service with prefill related logic. + /// The service used to validate data /// Service used to analyse files uploaded. /// Service used to validate files uploaded. public DataController( ILogger logger, - IInstance instanceClient, + IInstanceClient instanceClient, IInstantiationProcessor instantiationProcessor, - IData dataClient, - IDataProcessor dataProcessor, + IDataClient dataClient, + IEnumerable dataProcessors, IAppModel appModel, IAppResources appResourcesService, IPrefill prefillService, + IValidationService validationService, IFileAnalysisService fileAnalyserService, IFileValidationService fileValidationService, IAppMetadata appMetadata, @@ -80,11 +102,12 @@ public DataController( _instanceClient = instanceClient; _instantiationProcessor = instantiationProcessor; _dataClient = dataClient; - _dataProcessor = dataProcessor; + _dataProcessors = dataProcessors; _appModel = appModel; _appResourcesService = appResourcesService; _appMetadata = appMetadata; _prefillService = prefillService; + _validationService = validationService; _fileAnalyserService = fileAnalyserService; _fileValidationService = fileValidationService; _featureManager = featureManager; @@ -111,18 +134,13 @@ public async Task Create( [FromRoute] Guid instanceGuid, [FromQuery] string dataType) { - if (string.IsNullOrWhiteSpace(dataType)) - { - return BadRequest("Element type must be provided."); - } - /* The Body of the request is read much later when it has been made sure it is worth it. */ try { Application application = await _appMetadata.GetApplicationMetadata(); - - DataType dataTypeFromMetadata = application.DataTypes.FirstOrDefault(e => e.Id.Equals(dataType, StringComparison.InvariantCultureIgnoreCase)); + + DataType? dataTypeFromMetadata = application.DataTypes.First(e => e.Id.Equals(dataType, StringComparison.InvariantCultureIgnoreCase)); if (dataTypeFromMetadata == null) { @@ -156,7 +174,7 @@ public async Task Create( (bool validationRestrictionSuccess, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataTypeFromMetadata); if (!validationRestrictionSuccess) { - return new BadRequestObjectResult(await GetErrorDetails(errors)); + return BadRequest(await GetErrorDetails(errors)); } StreamContent streamContent = Request.CreateContentStream(); @@ -173,11 +191,11 @@ public async Task Create( Description = errorMessage }; _logger.LogError(errorMessage); - return new BadRequestObjectResult(await GetErrorDetails(new List { error })); + return BadRequest(await GetErrorDetails(new List { error })); } - + bool parseSuccess = Request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues); - string filename = parseSuccess ? DataRestrictionValidation.GetFileNameFromHeader(headerValues) : string.Empty; + string? filename = parseSuccess ? DataRestrictionValidation.GetFileNameFromHeader(headerValues) : null; IEnumerable fileAnalysisResults = new List(); if (FileAnalysisEnabledForDataType(dataTypeFromMetadata)) @@ -194,7 +212,7 @@ public async Task Create( if (!fileValidationSuccess) { - return new BadRequestObjectResult(await GetErrorDetails(validationIssues)); + return BadRequest(await GetErrorDetails(validationIssues)); } fileStream.Seek(0, SeekOrigin.Begin); @@ -256,24 +274,22 @@ public async Task Get( return NotFound($"Did not find instance {instance}"); } - DataElement dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); + DataElement? dataElement = instance.Data.First(m => m.Id.Equals(dataGuid.ToString())); if (dataElement == null) { return NotFound("Did not find data element"); } - string dataType = dataElement.DataType; - - bool? appLogic = await RequiresAppLogic(dataType); + DataType? dataType = await GetDataType(dataElement); - if (appLogic == null) + if (dataType is null) { string error = $"Could not determine if {dataType} requires app logic for application {org}/{app}"; _logger.LogError(error); return BadRequest(error); } - else if ((bool)appLogic) + else if (dataType.AppLogic?.ClassRef is not null) { return await GetFormData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid, dataType, instance); } @@ -317,32 +333,29 @@ public async Task Put( return Conflict($"Cannot update data element of archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}"); } - DataElement dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); + DataElement? dataElement = instance.Data.First(m => m.Id.Equals(dataGuid.ToString())); if (dataElement == null) { return NotFound("Did not find data element"); } - string dataType = dataElement.DataType; + DataType? dataType = await GetDataType(dataElement); - bool? appLogic = await RequiresAppLogic(dataType); - - if (appLogic == null) + if (dataType is null) { - _logger.LogError($"Could not determine if {dataType} requires app logic for application {org}/{app}"); + _logger.LogError("Could not determine if {dataType} requires app logic for application {org}/{app}", dataType, org, app); return BadRequest($"Could not determine if data type {dataType} requires application logic."); } - else if ((bool)appLogic) + else if (dataType.AppLogic?.ClassRef is not null) { return await PutFormData(org, app, instance, dataGuid, dataType); } - DataType dataTypeFromMetadata = (await _appMetadata.GetApplicationMetadata()).DataTypes.FirstOrDefault(e => e.Id.Equals(dataType, StringComparison.InvariantCultureIgnoreCase)); - (bool validationRestrictionSuccess, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataTypeFromMetadata); + (bool validationRestrictionSuccess, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataType); if (!validationRestrictionSuccess) { - return new BadRequestObjectResult(await GetErrorDetails(errors)); + return BadRequest(await GetErrorDetails(errors)); } return await PutBinaryData(instanceOwnerPartyId, instanceGuid, dataGuid); @@ -353,6 +366,172 @@ public async Task Put( } } + /// + /// Updates an existing form data element with a patch of changes. + /// + /// unique identfier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that is the owner of the instance + /// unique id to identify the instance + /// unique id to identify the data element to update + /// Container object for the and list of ignored validators + /// A response object with the new full model and validation issues from all the groups that run + [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)] + [HttpPatch("{dataGuid:guid}")] + [ProducesResponseType(typeof(DataPatchResponse), 200)] + [ProducesResponseType(typeof(ProblemDetails), 412)] + [ProducesResponseType(typeof(ProblemDetails), 422)] + public async Task> PatchFormData( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromRoute] Guid dataGuid, + [FromBody] DataPatchRequest dataPatchRequest) + { + try + { + var instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + + if (!InstanceIsActive(instance)) + { + return Conflict( + $"Cannot update data element of archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}"); + } + + var dataElement = instance.Data.First(m => m.Id.Equals(dataGuid.ToString())); + + if (dataElement == null) + { + return NotFound("Did not find data element"); + } + + var dataType = await GetDataType(dataElement); + + if (dataType?.AppLogic?.ClassRef is null) + { + _logger.LogError( + "Could not determine if {dataType} requires app logic for application {org}/{app}", + dataType, + org, + app); + return BadRequest($"Could not determine if data type {dataType?.Id} requires application logic."); + } + + var modelType = _appModel.GetModelType(dataType.AppLogic.ClassRef); + + var oldModel = + await _dataClient.GetFormData(instanceGuid, modelType, org, app, instanceOwnerPartyId, dataGuid); + + var (response, problemDetails) = + await PatchFormDataImplementation(dataType, dataElement, dataPatchRequest, oldModel, instance); + + if (problemDetails is not null) + { + return StatusCode(problemDetails.Status ?? 500, problemDetails); + } + + await UpdatePresentationTextsOnInstance(instance, dataType.Id, response.NewDataModel); + await UpdateDataValuesOnInstance(instance, dataType.Id, response.NewDataModel); + + // Save Formdata to database + await _dataClient.UpdateData( + response.NewDataModel, + instanceGuid, + modelType, + org, + app, + instanceOwnerPartyId, + dataGuid); + + return Ok(response); + } + catch (PlatformHttpException e) + { + return HandlePlatformHttpException(e, $"Unable to update data element {dataGuid} for instance {instanceOwnerPartyId}/{instanceGuid}"); + } + } + + /// + /// Part of that is separated out for testing purposes. + /// + /// The type of the data element + /// The data element + /// Container object for the and list of ignored validators + /// The old state of the form data + /// The instance + /// DataPatchResponse after this patch operation + internal async Task<(DataPatchResponse Response, ProblemDetails? Error)> PatchFormDataImplementation(DataType dataType, DataElement dataElement, DataPatchRequest dataPatchRequest, object oldModel, Instance instance) + { + var oldModelNode = JsonSerializer.SerializeToNode(oldModel); + var patchResult = dataPatchRequest.Patch.Apply(oldModelNode); + if (!patchResult.IsSuccess) + { + bool testOperationFailed = patchResult.Error!.Contains("is not equal to the indicated value."); + return (null!, new ProblemDetails() + { + Title = testOperationFailed ? "Precondition in patch failed" : "Patch Operation Failed", + Detail = patchResult.Error, + Type = "https://datatracker.ietf.org/doc/html/rfc6902/", + Status = testOperationFailed ? (int)HttpStatusCode.PreconditionFailed : (int)HttpStatusCode.UnprocessableContent, + Extensions = new Dictionary() + { + { "previousModel", oldModel }, + { "patchOperationIndex", patchResult.Operation }, + } + }); + } + + var (model, error) = DeserializeModel(oldModel.GetType(), patchResult.Result!); + if (error is not null) + { + return (null!, new ProblemDetails() + { + Title = "Patch operation did not deserialize", + Detail = error, + Type = "https://datatracker.ietf.org/doc/html/rfc6902/", + Status = (int)HttpStatusCode.UnprocessableContent, + }); + } + + foreach (var dataProcessor in _dataProcessors) + { + await dataProcessor.ProcessDataWrite(instance, Guid.Parse(dataElement.Id), model, oldModel); + } + + // Ensure that all lists are changed from null to empty list. + ObjectUtils.InitializeListsAndNullEmptyStrings(model); + + var changedFields = dataPatchRequest.Patch.Operations.Select(o => o.Path.ToString()).ToList(); + + var validationIssues = await _validationService.ValidateFormData(instance, dataElement, dataType, model, changedFields, dataPatchRequest.IgnoredValidators); + var response = new DataPatchResponse + { + NewDataModel = model, + ValidationIssues = validationIssues + }; + return (response, null); + } + + private static (object Model, string? Error) DeserializeModel(Type type, JsonNode patchResult) + { + try + { + var model = patchResult.Deserialize(type, JsonSerializerOptions); + if (model is null) + { + return (null!, "Deserialize patched model returned null"); + } + + return (model, null); + } + catch (JsonException e) when (e.Message.Contains("could not be mapped to any .NET member contained in type")) + { + // Give better feedback when the issue is that the patch contains a path that does not exist in the model + return (null!, e.Message); + } + } + /// /// Delete a data element. /// @@ -384,24 +563,22 @@ public async Task Delete( return Conflict($"Cannot delete data element of archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}"); } - DataElement dataElement = instance.Data.Find(m => m.Id.Equals(dataGuid.ToString())); + DataElement? dataElement = instance.Data.Find(m => m.Id.Equals(dataGuid.ToString())); if (dataElement == null) { return NotFound("Did not find data element"); } - string dataType = dataElement.DataType; - - bool? appLogic = await RequiresAppLogic(dataType); + DataType? dataType = await GetDataType(dataElement); - if (appLogic == null) + if (dataType == null) { - string errorMsg = $"Could not determine if {dataType} requires app logic for application {org}/{app}"; + string errorMsg = $"Could not determine if {dataElement.DataType} requires app logic for application {org}/{app}"; _logger.LogError(errorMsg); return BadRequest(errorMsg); } - else if ((bool)appLogic) + else if (dataType.AppLogic?.ClassRef is not null) { // trying deleting a form element return BadRequest("Deleting form data is not possible at this moment."); @@ -419,21 +596,19 @@ private ActionResult ExceptionResponse(Exception exception, string message) { _logger.LogError(exception, message); - if (exception is PlatformHttpException) + if (exception is PlatformHttpException phe) { - PlatformHttpException phe = exception as PlatformHttpException; return StatusCode((int)phe.Response.StatusCode, phe.Message); } - else if (exception is ServiceException) + else if (exception is ServiceException se) { - ServiceException se = exception as ServiceException; return StatusCode((int)se.StatusCode, se.Message); } return StatusCode(500, $"{message}"); } - private async Task CreateBinaryData(Instance instanceBefore, string dataType, string contentType, string filename, Stream fileStream) + private async Task CreateBinaryData(Instance instanceBefore, string dataType, string contentType, string? filename, Stream fileStream) { int instanceOwnerPartyId = int.Parse(instanceBefore.Id.Split("/")[0]); Guid instanceGuid = Guid.Parse(instanceBefore.Id.Split("/")[1]); @@ -457,7 +632,7 @@ private async Task CreateAppModelData( { Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); - object appModel; + object? appModel; string classRef = _appResourcesService.GetClassRefForLogicDataType(dataType); @@ -470,7 +645,7 @@ private async Task CreateAppModelData( ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (!string.IsNullOrEmpty(deserializer.Error)) + if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) { return BadRequest(deserializer.Error); } @@ -508,7 +683,7 @@ private async Task GetBinaryData( if (dataStream != null) { - string userOrgClaim = User.GetOrg(); + string? userOrgClaim = User.GetOrg(); if (userOrgClaim == null || !org.Equals(userOrgClaim, StringComparison.InvariantCultureIgnoreCase)) { await _instanceClient.UpdateReadStatus(instanceOwnerPartyId, instanceGuid, "read"); @@ -536,21 +711,10 @@ private async Task DeleteBinaryData(string org, string app, int in } } - private async Task RequiresAppLogic(string dataType) + private async Task GetDataType(DataElement element) { - bool? appLogic = false; - - try - { - Application application = await _appMetadata.GetApplicationMetadata(); - appLogic = application?.DataTypes.Where(e => e.Id == dataType).Select(e => e.AppLogic?.ClassRef != null).First(); - } - catch (Exception) - { - appLogic = null; - } - - return appLogic; + Application application = await _appMetadata.GetApplicationMetadata(); + return application?.DataTypes.Find(e => e.Id == element.DataType); } /// @@ -565,15 +729,13 @@ private async Task GetFormData( int instanceOwnerId, Guid instanceGuid, Guid dataGuid, - string dataType, + DataType dataType, Instance instance) { - string appModelclassRef = _appResourcesService.GetClassRefForLogicDataType(dataType); - // Get Form Data from data service. Assumes that the data element is form data. object appModel = await _dataClient.GetFormData( instanceGuid, - _appModel.GetModelType(appModelclassRef), + _appModel.GetModelType(dataType.AppLogic.ClassRef), org, app, instanceOwnerId, @@ -584,9 +746,13 @@ private async Task GetFormData( return BadRequest($"Did not find form data for data element {dataGuid}"); } - await _dataProcessor.ProcessDataRead(instance, dataGuid, appModel); + foreach (var dataProcessor in _dataProcessors) + { + _logger.LogInformation("ProcessDataRead for {modelType} using {dataProcesor}", appModel.GetType().Name, dataProcessor.GetType().Name); + await dataProcessor.ProcessDataRead(instance, dataGuid, appModel); + } - string userOrgClaim = User.GetOrg(); + string? userOrgClaim = User.GetOrg(); if (userOrgClaim == null || !org.Equals(userOrgClaim, StringComparison.InvariantCultureIgnoreCase)) { await _instanceClient.UpdateReadStatus(instanceOwnerId, instanceGuid, "read"); @@ -600,7 +766,7 @@ private async Task PutBinaryData(int instanceOwnerPartyId, Guid in if (Request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues)) { var contentDispositionHeader = ContentDispositionHeaderValue.Parse(headerValues.ToString()); - _logger.LogInformation("Content-Disposition: {ContentDisposition}", headerValues); + _logger.LogInformation("Content-Disposition: {ContentDisposition}", headerValues.ToString()); DataElement dataElement = await _dataClient.UpdateBinaryData(new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), Request.ContentType, contentDispositionHeader.FileName.ToString(), dataGuid, Request.Body); SelfLinkHelper.SetDataAppSelfLinks(instanceOwnerPartyId, instanceGuid, dataElement, Request); @@ -610,15 +776,15 @@ private async Task PutBinaryData(int instanceOwnerPartyId, Guid in return BadRequest("Invalid data provided. Error: The request must include a Content-Disposition header"); } - private async Task PutFormData(string org, string app, Instance instance, Guid dataGuid, string dataType) + private async Task PutFormData(string org, string app, Instance instance, Guid dataGuid, DataType dataType) { int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); - string classRef = _appResourcesService.GetClassRefForLogicDataType(dataType); + string classRef = dataType.AppLogic.ClassRef; Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); - object serviceModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); + object? serviceModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); if (!string.IsNullOrEmpty(deserializer.Error)) { @@ -630,10 +796,10 @@ private async Task PutFormData(string org, string app, Instance in return BadRequest("No data found in content"); } - Dictionary changedFields = await JsonHelper.ProcessDataWriteWithDiff(instance, dataGuid, serviceModel, _dataProcessor, _logger); + Dictionary? changedFields = await JsonHelper.ProcessDataWriteWithDiff(instance, dataGuid, serviceModel, _dataProcessors, _logger); - await UpdatePresentationTextsOnInstance(instance, dataType, serviceModel); - await UpdateDataValuesOnInstance(instance, dataType, serviceModel); + await UpdatePresentationTextsOnInstance(instance, dataType.Id, serviceModel); + await UpdateDataValuesOnInstance(instance, dataType.Id, serviceModel); // Save Formdata to database DataElement updatedDataElement = await _dataClient.UpdateData( @@ -650,8 +816,10 @@ private async Task PutFormData(string org, string app, Instance in string dataUrl = updatedDataElement.SelfLinks.Apps; if (changedFields is not null) { - CalculationResult calculationResult = new CalculationResult(updatedDataElement); - calculationResult.ChangedFields = changedFields; + CalculationResult calculationResult = new(updatedDataElement) + { + ChangedFields = changedFields + }; return StatusCode((int)HttpStatusCode.SeeOther, calculationResult); } diff --git a/src/Altinn.App.Api/Controllers/DataTagsController.cs b/src/Altinn.App.Api/Controllers/DataTagsController.cs index 83eb68636..8ffda6c36 100644 --- a/src/Altinn.App.Api/Controllers/DataTagsController.cs +++ b/src/Altinn.App.Api/Controllers/DataTagsController.cs @@ -1,17 +1,16 @@ -using System; -using System.Linq; +#nullable enable using System.Net.Mime; using System.Text.RegularExpressions; -using System.Threading.Tasks; using Altinn.App.Api.Infrastructure.Filters; using Altinn.App.Api.Models; using Altinn.App.Core.Constants; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Infrastructure.Clients; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Instances; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Altinn.App.Api.Controllers @@ -26,8 +25,8 @@ namespace Altinn.App.Api.Controllers [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/data/{dataGuid:guid}/tags")] public class DataTagsController : ControllerBase { - private readonly IInstance _instanceClient; - private readonly IData _dataClient; + private readonly IInstanceClient _instanceClient; + private readonly IDataClient _dataClient; /// /// Initialize a new instance of with the given services. @@ -35,8 +34,8 @@ public class DataTagsController : ControllerBase /// A client that can be used to send instance requests to storage. /// A client that can be used to send data requests to storage. public DataTagsController( - IInstance instanceClient, - IData dataClient) + IInstanceClient instanceClient, + IDataClient dataClient) { _instanceClient = instanceClient; _dataClient = dataClient; @@ -67,7 +66,7 @@ public async Task> Get( return NotFound($"Unable to find instance based on the given parameters."); } - DataElement dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); + DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); if (dataElement == null) { @@ -114,7 +113,7 @@ public async Task> Add( return NotFound("Unable to find instance based on the given parameters."); } - DataElement dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); + DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); if (dataElement == null) { @@ -163,7 +162,7 @@ public async Task Delete( return NotFound("Unable to find instance based on the given parameters."); } - DataElement dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); + DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); if (dataElement == null) { diff --git a/src/Altinn.App.Api/Controllers/FileScanController.cs b/src/Altinn.App.Api/Controllers/FileScanController.cs index 805fd95f1..6b94fc7a8 100644 --- a/src/Altinn.App.Api/Controllers/FileScanController.cs +++ b/src/Altinn.App.Api/Controllers/FileScanController.cs @@ -1,11 +1,9 @@ -using System; -using System.Threading.Tasks; -using Altinn.App.Api.Models; -using Altinn.App.Core.Interface; +using Altinn.App.Api.Models; +using Altinn.App.Core.Infrastructure.Clients; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Altinn.App.Api.Controllers @@ -17,12 +15,12 @@ namespace Altinn.App.Api.Controllers [ApiController] public class FileScanController : ControllerBase { - private readonly IInstance _instanceClient; + private readonly IInstanceClient _instanceClient; /// /// Initialises a new instance of the class /// - public FileScanController(IInstance instanceClient) + public FileScanController(IInstanceClient instanceClient) { _instanceClient = instanceClient; } diff --git a/src/Altinn.App.Api/Controllers/HomeController.cs b/src/Altinn.App.Api/Controllers/HomeController.cs index f0f931b35..282a26f8a 100644 --- a/src/Altinn.App.Api/Controllers/HomeController.cs +++ b/src/Altinn.App.Api/Controllers/HomeController.cs @@ -2,7 +2,6 @@ using System.Text.Json; using System.Web; using Altinn.App.Core.Configuration; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; diff --git a/src/Altinn.App.Api/Controllers/InstancesController.cs b/src/Altinn.App.Api/Controllers/InstancesController.cs index aec29404d..c6fbe0f87 100644 --- a/src/Altinn.App.Api/Controllers/InstancesController.cs +++ b/src/Altinn.App.Api/Controllers/InstancesController.cs @@ -2,7 +2,6 @@ using System.Net; using System.Text; - using Altinn.App.Api.Helpers.RequestHandling; using Altinn.App.Api.Infrastructure.Filters; using Altinn.App.Api.Mappers; @@ -13,10 +12,17 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Helpers.Serialization; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Events; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Prefill; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.Validation; using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Helpers; @@ -25,12 +31,10 @@ using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Models; - using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; - using Newtonsoft.Json; namespace Altinn.App.Api.Controllers @@ -48,11 +52,11 @@ public class InstancesController : ControllerBase { private readonly ILogger _logger; - private readonly IInstance _instanceClient; - private readonly IData _dataClient; - private readonly IRegister _registerClient; - private readonly IEvents _eventsService; - private readonly IProfile _profileClientClient; + private readonly IInstanceClient _instanceClient; + private readonly IDataClient _dataClient; + private readonly IAltinnPartyClient _altinnPartyClientClient; + private readonly IEventsClient _eventsClient; + private readonly IProfileClient _profileClient; private readonly IAppMetadata _appMetadata; private readonly IAppModel _appModel; @@ -60,8 +64,9 @@ public class InstancesController : ControllerBase private readonly IInstantiationValidator _instantiationValidator; private readonly IPDP _pdp; private readonly IPrefill _prefillService; - private readonly IProcessEngine _processEngine; private readonly AppSettings _appSettings; + private readonly IProcessEngine _processEngine; + private readonly IOrganizationClient _orgClient; private const long RequestSizeLimit = 2000 * 1024 * 1024; @@ -70,34 +75,36 @@ public class InstancesController : ControllerBase /// public InstancesController( ILogger logger, - IRegister registerClient, - IInstance instanceClient, - IData dataClient, + IAltinnPartyClient altinnPartyClientClient, + IInstanceClient instanceClient, + IDataClient dataClient, IAppMetadata appMetadata, IAppModel appModel, IInstantiationProcessor instantiationProcessor, IInstantiationValidator instantiationValidator, IPDP pdp, - IEvents eventsService, + IEventsClient eventsClient, IOptions appSettings, IPrefill prefillService, - IProfile profileClient, - IProcessEngine processEngine) + IProfileClient profileClient, + IProcessEngine processEngine, + IOrganizationClient orgClient) { _logger = logger; _instanceClient = instanceClient; _dataClient = dataClient; _appMetadata = appMetadata; - _registerClient = registerClient; + _altinnPartyClientClient = altinnPartyClientClient; _appModel = appModel; _instantiationProcessor = instantiationProcessor; _instantiationValidator = instantiationValidator; _pdp = pdp; - _eventsService = eventsService; + _eventsClient = eventsClient; _appSettings = appSettings.Value; _prefillService = prefillService; - _profileClientClient = profileClient; + _profileClient = profileClient; _processEngine = processEngine; + _orgClient = orgClient; } /// @@ -208,16 +215,17 @@ public async Task> Post( } else { + // create minimum instance template instanceTemplate = new Instance { - InstanceOwner = new InstanceOwner { PartyId = instanceOwnerPartyId.Value.ToString() } + InstanceOwner = new InstanceOwner { PartyId = instanceOwnerPartyId!.Value.ToString() } }; } ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); RequestPartValidator requestValidator = new RequestPartValidator(application); - string multipartError = requestValidator.ValidateParts(parsedRequest.Parts); + string? multipartError = requestValidator.ValidateParts(parsedRequest.Parts); if (!string.IsNullOrEmpty(multipartError)) { @@ -227,7 +235,7 @@ public async Task> Post( Party party; try { - party = await LookupParty(instanceTemplate.InstanceOwner); + party = await LookupParty(instanceTemplate.InstanceOwner) ?? throw new Exception("Unknown party"); instanceTemplate.InstanceOwner = InstantiationHelper.PartyToInstanceOwner(party); } catch (Exception partyLookupException) @@ -267,12 +275,24 @@ public async Task> Post( Instance instance; instanceTemplate.Process = null; - ProcessChangeContext processChangeContext = new ProcessChangeContext(instanceTemplate, User); + ProcessStateChange? change = null; + try { - // start process - processChangeContext.DontUpdateProcessAndDispatchEvents = true; - processChangeContext = await _processEngine.StartProcess(processChangeContext); + // start process and goto next task + ProcessStartRequest processStartRequest = new ProcessStartRequest + { + Instance = instanceTemplate, + User = User, + Dryrun = true + }; + var result = await _processEngine.StartProcess(processStartRequest); + if (!result.Success) + { + return Conflict(result.ErrorMessage); + } + + change = result.ProcessStateChange; // create the instance instance = await _instanceClient.CreateInstance(org, app, instanceTemplate); @@ -290,9 +310,14 @@ public async Task> Post( instance = await _instanceClient.GetInstance(app, org, int.Parse(instance.InstanceOwner.PartyId), Guid.Parse(instance.Id.Split("/")[1])); // notify app and store events - processChangeContext.Instance = instance; - processChangeContext.DontUpdateProcessAndDispatchEvents = false; - await _processEngine.StartTask(processChangeContext); + var request = new ProcessStartRequest() + { + Instance = instance, + User = User, + Dryrun = false, + }; + _logger.LogInformation("Events sent to process engine: {Events}", change?.Events); + await _processEngine.UpdateInstanceAndRerunEvents(request, change?.Events); } catch (Exception exception) { @@ -353,7 +378,7 @@ public async Task> PostSimplified( Party party; try { - party = await LookupParty(instansiationInstance.InstanceOwner); + party = await LookupParty(instansiationInstance.InstanceOwner) ?? throw new Exception("Unknown party"); instansiationInstance.InstanceOwner = InstantiationHelper.PartyToInstanceOwner(party); } catch (Exception partyLookupException) @@ -404,15 +429,21 @@ public async Task> PostSimplified( } Instance instance; + ProcessChangeResult processResult; try { + // start process and goto next task instanceTemplate.Process = null; - // start process - ProcessChangeContext processChangeContext = new ProcessChangeContext(instanceTemplate, User); - processChangeContext.Prefill = instansiationInstance.Prefill; - processChangeContext.DontUpdateProcessAndDispatchEvents = true; - processChangeContext = await _processEngine.StartProcess(processChangeContext); + var request = new ProcessStartRequest() + { + Instance = instanceTemplate, + User = User, + Dryrun = true, + Prefill = instansiationInstance.Prefill + }; + + processResult = await _processEngine.StartProcess(request); Instance? source = null; @@ -438,16 +469,21 @@ public async Task> PostSimplified( instance = await _instanceClient.CreateInstance(org, app, instanceTemplate); - if (copySourceInstance) + if (copySourceInstance && source is not null) { await CopyDataFromSourceInstance(application, instance, source); } instance = await _instanceClient.GetInstance(instance); - processChangeContext.Instance = instance; - processChangeContext.DontUpdateProcessAndDispatchEvents = false; - await _processEngine.StartTask(processChangeContext); + var updateRequest = new ProcessStartRequest() + { + Instance = instance, + User = User, + Dryrun = false, + Prefill = instansiationInstance.Prefill + }; + await _processEngine.UpdateInstanceAndRerunEvents(updateRequest, processResult.ProcessStateChange?.Events); } catch (Exception exception) { @@ -535,12 +571,14 @@ public async Task CopyInstance( { return StatusCode((int)HttpStatusCode.Forbidden, validationResult); } - - ProcessChangeContext processChangeContext = new(targetInstance, User) + + ProcessStartRequest processStartRequest = new() { - DontUpdateProcessAndDispatchEvents = true + Instance = targetInstance, + User = User, + Dryrun = true }; - processChangeContext = await _processEngine.StartProcess(processChangeContext); + var startResult = await _processEngine.StartProcess(processStartRequest); targetInstance = await _instanceClient.CreateInstance(org, app, targetInstance); @@ -548,9 +586,13 @@ public async Task CopyInstance( targetInstance = await _instanceClient.GetInstance(targetInstance); - processChangeContext.Instance = targetInstance; - processChangeContext.DontUpdateProcessAndDispatchEvents = false; - await _processEngine.StartTask(processChangeContext); + ProcessStartRequest rerunRequest = new() + { + Instance = targetInstance, + Dryrun = false, + User = User + }; + await _processEngine.UpdateInstanceAndRerunEvents(rerunRequest, startResult.ProcessStateChange?.Events); await RegisterEvent("app.instance.created", targetInstance); @@ -705,7 +747,7 @@ public async Task>> GetActiveInstances([FromRo { if (lastChangedBy?.Length == 9) { - Organization? organization = await _registerClient.ER.GetOrganization(lastChangedBy); + Organization? organization = await _orgClient.GetOrganization(lastChangedBy); if (organization is not null && !string.IsNullOrEmpty(organization.Name)) { userAndOrgLookup.Add(lastChangedBy, organization.Name); @@ -713,7 +755,7 @@ public async Task>> GetActiveInstances([FromRo } else if (int.TryParse(lastChangedBy, out int lastChangedByInt)) { - UserProfile? user = await _profileClientClient.GetUserProfile(lastChangedByInt); + UserProfile? user = await _profileClient.GetUserProfile(lastChangedByInt); if (user is not null && user.Party is not null && !string.IsNullOrEmpty(user.Party.Name)) { userAndOrgLookup.Add(lastChangedBy, user.Party.Name); @@ -859,13 +901,13 @@ private async Task AuthorizeAction(string org, string app, in return enforcementResult; } - private async Task LookupParty(InstanceOwner instanceOwner) + private async Task LookupParty(InstanceOwner instanceOwner) { if (instanceOwner.PartyId != null) { try { - return await _registerClient.GetParty(int.Parse(instanceOwner.PartyId)); + return await _altinnPartyClientClient.GetParty(int.Parse(instanceOwner.PartyId)); } catch (Exception e) when (e is not ServiceException) { @@ -882,12 +924,12 @@ private async Task LookupParty(InstanceOwner instanceOwner) if (!string.IsNullOrEmpty(instanceOwner.PersonNumber)) { lookupNumber = "personNumber"; - return await _registerClient.LookupParty(new PartyLookup { Ssn = instanceOwner.PersonNumber }); + return await _altinnPartyClientClient.LookupParty(new PartyLookup { Ssn = instanceOwner.PersonNumber }); } else if (!string.IsNullOrEmpty(instanceOwner.OrganisationNumber)) { lookupNumber = "organisationNumber"; - return await _registerClient.LookupParty(new PartyLookup { OrgNo = instanceOwner.OrganisationNumber }); + return await _altinnPartyClientClient.LookupParty(new PartyLookup { OrgNo = instanceOwner.OrganisationNumber }); } else { @@ -916,7 +958,7 @@ private async Task StorePrefillParts(Instance instance, ApplicationMetadata appI DataElement dataElement; if (dataType?.AppLogic?.ClassRef != null) { - _logger.LogInformation($"Storing part {part.Name}"); + _logger.LogInformation("Storing part {partName}", part.Name); Type type; try @@ -931,12 +973,12 @@ private async Task StorePrefillParts(Instance instance, ApplicationMetadata appI ModelDeserializer deserializer = new ModelDeserializer(_logger, type); object? data = await deserializer.DeserializeAsync(part.Stream, part.ContentType); - if (!string.IsNullOrEmpty(deserializer.Error)) + if (!string.IsNullOrEmpty(deserializer.Error) || data is null) { throw new InvalidOperationException(deserializer.Error); } - await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, part.Name, data); + await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, part.Name!, data); await _instantiationProcessor.DataCreation(instance, data, null); @@ -947,11 +989,11 @@ private async Task StorePrefillParts(Instance instance, ApplicationMetadata appI org, app, instanceOwnerIdAsInt, - part.Name); + part.Name!); } else { - dataElement = await _dataClient.InsertBinaryData(instance.Id, part.Name, part.ContentType, part.FileName, part.Stream); + dataElement = await _dataClient.InsertBinaryData(instance.Id, part.Name!, part.ContentType, part.FileName, part.Stream); } if (dataElement == null) @@ -1000,7 +1042,7 @@ private async Task RegisterEvent(string eventType, Instance instance) { try { - await _eventsService.AddEvent(eventType, instance); + await _eventsClient.AddEvent(eventType, instance); } catch (Exception exception) { diff --git a/src/Altinn.App.Api/Controllers/OptionsController.cs b/src/Altinn.App.Api/Controllers/OptionsController.cs index 636b9777b..1d6c64057 100644 --- a/src/Altinn.App.Api/Controllers/OptionsController.cs +++ b/src/Altinn.App.Api/Controllers/OptionsController.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -38,16 +40,16 @@ public OptionsController(IAppOptionsService appOptionsService) [HttpGet("{optionsId}")] public async Task Get( [FromRoute] string optionsId, - [FromQuery] string language, + [FromQuery] string? language, [FromQuery] Dictionary queryParams) { - AppOptions appOptions = await _appOptionsService.GetOptionsAsync(optionsId, language, queryParams); + AppOptions appOptions = await _appOptionsService.GetOptionsAsync(optionsId, language ?? "nb", queryParams); if (appOptions.Options == null) { return NotFound(); } - HttpContext.Response.Headers.Add("Altinn-DownstreamParameters", appOptions.Parameters.ToNameValueString(',')); + HttpContext.Response.Headers.Append("Altinn-DownstreamParameters", appOptions.Parameters.ToUrlEncodedNameValueString(',')); return Ok(appOptions.Options); } @@ -74,12 +76,12 @@ public async Task Get( [FromRoute] int instanceOwnerPartyId, [FromRoute] Guid instanceGuid, [FromRoute] string optionsId, - [FromQuery] string language, + [FromQuery] string? language, [FromQuery] Dictionary queryParams) { var instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId, instanceGuid); - AppOptions appOptions = await _appOptionsService.GetOptionsAsync(instanceIdentifier, optionsId, language, queryParams); + AppOptions appOptions = await _appOptionsService.GetOptionsAsync(instanceIdentifier, optionsId, language ?? "nb", queryParams); // Only return NotFound if we can't find an options provider. // If we find the options provider, but it doesnt' have values, return empty list. @@ -88,7 +90,7 @@ public async Task Get( return NotFound(); } - HttpContext.Response.Headers.Add("Altinn-DownstreamParameters", appOptions.Parameters.ToNameValueString(',')); + HttpContext.Response.Headers.Append("Altinn-DownstreamParameters", appOptions.Parameters.ToUrlEncodedNameValueString(',')); return Ok(appOptions.Options); } diff --git a/src/Altinn.App.Api/Controllers/PagesController.cs b/src/Altinn.App.Api/Controllers/PagesController.cs index 08df0534a..2eff128fe 100644 --- a/src/Altinn.App.Api/Controllers/PagesController.cs +++ b/src/Altinn.App.Api/Controllers/PagesController.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Altinn.App.Core.Features; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; using Altinn.App.Core.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -19,6 +15,7 @@ namespace Altinn.App.Api.Controllers [Authorize] [ApiController] [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/pages")] + [Obsolete("IPageOrder does not work with frontend version 4")] public class PagesController : ControllerBase { private readonly IAppModel _appModel; @@ -35,8 +32,8 @@ public class PagesController : ControllerBase /// The page order service public PagesController( IAppModel appModel, - IAppResources resources, - IPageOrder pageOrder, + IAppResources resources, + IPageOrder pageOrder, ILogger logger) { _appModel = appModel; diff --git a/src/Altinn.App.Api/Controllers/PartiesController.cs b/src/Altinn.App.Api/Controllers/PartiesController.cs index 5a81343ae..68a5de46c 100644 --- a/src/Altinn.App.Api/Controllers/PartiesController.cs +++ b/src/Altinn.App.Api/Controllers/PartiesController.cs @@ -1,8 +1,10 @@ #nullable enable using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Profile.Models; @@ -21,9 +23,9 @@ namespace Altinn.App.Api.Controllers [ApiController] public class PartiesController : ControllerBase { - private readonly IAuthorization _authorizationClient; + private readonly IAuthorizationClient _authorizationClient; private readonly UserHelper _userHelper; - private readonly IProfile _profileClient; + private readonly IProfileClient _profileClient; private readonly GeneralSettings _settings; private readonly IAppMetadata _appMetadata; @@ -31,14 +33,14 @@ public class PartiesController : ControllerBase /// Initializes a new instance of the class /// public PartiesController( - IAuthorization authorizationClient, - IProfile profileClient, - IRegister registerClient, + IAuthorizationClient authorizationClient, + IProfileClient profileClient, + IAltinnPartyClient altinnPartyClientClient, IOptions settings, IAppMetadata appMetadata) { _authorizationClient = authorizationClient; - _userHelper = new UserHelper(profileClient, registerClient, settings); + _userHelper = new UserHelper(profileClient, altinnPartyClientClient, settings); _profileClient = profileClient; _settings = settings.Value; _appMetadata = appMetadata; diff --git a/src/Altinn.App.Api/Controllers/PdfController.cs b/src/Altinn.App.Api/Controllers/PdfController.cs index bc9bb481a..62692483e 100644 --- a/src/Altinn.App.Api/Controllers/PdfController.cs +++ b/src/Altinn.App.Api/Controllers/PdfController.cs @@ -2,8 +2,10 @@ using System.Text.Json; using Altinn.App.Core.Features; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Authorization; @@ -19,11 +21,11 @@ namespace Altinn.App.Api.Controllers [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/data/{dataGuid:guid}/pdf")] public class PdfController : ControllerBase { - private readonly IInstance _instanceClient; + private readonly IInstanceClient _instanceClient; private readonly IPdfFormatter _pdfFormatter; private readonly IAppResources _resources; private readonly IAppModel _appModel; - private readonly IData _dataClient; + private readonly IDataClient _dataClient; /// /// Initializes a new instance of the class. @@ -34,11 +36,11 @@ public class PdfController : ControllerBase /// The app model service /// The data client public PdfController( - IInstance instanceClient, + IInstanceClient instanceClient, IPdfFormatter pdfFormatter, IAppResources resources, IAppModel appModel, - IData dataClient) + IDataClient dataClient) { _instanceClient = instanceClient; _pdfFormatter = pdfFormatter; diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 6d6926e83..a8959ff0d 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -1,29 +1,23 @@ -using System; -using System.Collections.Generic; -using System.Linq; +#nullable enable using System.Net; -using System.Threading.Tasks; - using Altinn.App.Api.Infrastructure.Filters; +using Altinn.App.Api.Models; +using Altinn.App.Core.Features; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; -using Altinn.App.Core.Internal.Process.Elements.Base; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.Validation; -using Altinn.Authorization.ABAC.Xacml.JsonProfile; -using Altinn.Common.PEP.Helpers; -using Altinn.Common.PEP.Interfaces; using Altinn.Platform.Storage.Interface.Models; - using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; +using AppProcessState = Altinn.App.Core.Internal.Process.Elements.AppProcessState; +using IAuthorizationService = Altinn.App.Core.Internal.Auth.IAuthorizationService; namespace Altinn.App.Api.Controllers { @@ -39,35 +33,32 @@ public class ProcessController : ControllerBase private const int MaxIterationsAllowed = 100; private readonly ILogger _logger; - private readonly IInstance _instanceClient; - private readonly IProcess _processService; - private readonly IValidation _validationService; - private readonly IPDP _pdp; + private readonly IInstanceClient _instanceClient; + private readonly IProcessClient _processClient; + private readonly IValidationService _validationService; + private readonly IAuthorizationService _authorization; private readonly IProcessEngine _processEngine; private readonly IProcessReader _processReader; - private readonly IFlowHydration _flowHydration; /// /// Initializes a new instance of the /// public ProcessController( ILogger logger, - IInstance instanceClient, - IProcess processService, - IValidation validationService, - IPDP pdp, - IProcessEngine processEngine, + IInstanceClient instanceClient, + IProcessClient processClient, + IValidationService validationService, + IAuthorizationService authorization, IProcessReader processReader, - IFlowHydration flowHydration) + IProcessEngine processEngine) { _logger = logger; _instanceClient = instanceClient; - _processService = processService; + _processClient = processClient; _validationService = validationService; - _pdp = pdp; - _processEngine = processEngine; + _authorization = authorization; _processReader = processReader; - _flowHydration = flowHydration; + _processEngine = processEngine; } /// @@ -82,7 +73,7 @@ public ProcessController( [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Authorize(Policy = "InstanceRead")] - public async Task> GetProcessState( + public async Task> GetProcessState( [FromRoute] string org, [FromRoute] string app, [FromRoute] int instanceOwnerPartyId, @@ -91,9 +82,9 @@ public async Task> GetProcessState( try { Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - ProcessState processState = instance.Process; + AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, instance.Process); - return Ok(processState); + return Ok(appProcessState); } catch (PlatformHttpException e) { @@ -120,28 +111,34 @@ public async Task> GetProcessState( [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] [Authorize(Policy = "InstanceInstantiate")] - public async Task> StartProcess( + public async Task> StartProcess( [FromRoute] string org, [FromRoute] string app, [FromRoute] int instanceOwnerPartyId, [FromRoute] Guid instanceGuid, - [FromQuery] string startEvent = null) + [FromQuery] string? startEvent = null) { - Instance instance = null; + Instance? instance = null; try { instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - ProcessChangeContext changeContext = new ProcessChangeContext(instance, User); - changeContext.RequestedProcessElementId = startEvent; - changeContext = await _processEngine.StartProcess(changeContext); - if (changeContext.FailedProcessChange) + var request = new ProcessStartRequest() + { + Instance = instance, + StartEventId = startEvent, + User = User, + Dryrun = false + }; + var result = await _processEngine.StartProcess(request); + if (!result.Success) { - return Conflict(changeContext.ProcessMessages[0].Message); + return Conflict(result.ErrorMessage); } - - return Ok(changeContext.Instance.Process); + + AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, result.ProcessStateChange?.NewProcessState); + return Ok(appProcessState); } catch (PlatformHttpException e) { @@ -167,14 +164,15 @@ public async Task> StartProcess( [HttpGet("next")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status409Conflict)] + [Obsolete("From v8 of nuget package navigation is done by sending performed action to the next api. Available actions are returned in the GET /process endpoint")] public async Task>> GetNextElements( [FromRoute] string org, [FromRoute] string app, [FromRoute] int instanceOwnerPartyId, [FromRoute] Guid instanceGuid) { - Instance instance = null; - string currentTaskId = null; + Instance? instance = null; + string? currentTaskId = null; try { @@ -191,10 +189,8 @@ public async Task>> GetNextElements( { return Conflict($"Instance does not have valid info about currentTask"); } - - List nextElements = await _flowHydration.NextFollowAndFilterGateways(instance, currentTaskId, false); - return Ok(nextElements.Select(e => e.Id).ToList()); + return Ok(new List()); } catch (PlatformHttpException e) { @@ -207,24 +203,11 @@ public async Task>> GetNextElements( } } - private async Task CanTaskBeEnded(Instance instance, string currentElementId) + private async Task CanTaskBeEnded(Instance instance, string currentTaskId) { - List validationIssues = new List(); + var validationIssues = await _validationService.ValidateInstanceAtTask(instance, currentTaskId); - bool canEndTask; - - if (instance.Process?.CurrentTask?.Validated == null || !instance.Process.CurrentTask.Validated.CanCompleteTask) - { - validationIssues = await _validationService.ValidateAndUpdateProcess(instance, currentElementId); - - canEndTask = await ProcessHelper.CanEndProcessTask(instance, validationIssues); - } - else - { - canEndTask = await ProcessHelper.CanEndProcessTask(instance, validationIssues); - } - - return canEndTask; + return await ProcessHelper.CanEndProcessTask(instance, validationIssues); } /// @@ -235,8 +218,7 @@ private async Task CanTaskBeEnded(Instance instance, string currentElement /// application identifier which is unique within an organisation /// unique id of the party that is the owner of the instance /// unique id to identify the instance - /// the id of the next element to move to. Query parameter is optional, - /// but must be specified if more than one element can be reached from the current process ellement. + /// obsolete: alias for action /// Optional parameter to pass on the language used in the form if this differs from the profile language, /// which otherwise is used automatically. The language is picked up when generating the PDF when leaving a step, /// and is not used for anything else. @@ -245,19 +227,25 @@ private async Task CanTaskBeEnded(Instance instance, string currentElement [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task> NextElement( + public async Task> NextElement( [FromRoute] string org, [FromRoute] string app, [FromRoute] int instanceOwnerPartyId, [FromRoute] Guid instanceGuid, - [FromQuery] string elementId = null, - [FromQuery] string lang = null) + [FromQuery] string? elementId = null, + [FromQuery] string? lang = null) { try { + ProcessNext? processNext = null; + if (Request.Body != null && Request.Body.CanRead) + { + processNext = await DeserializeFromStream(Request.Body); + } + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - if (instance.Process == null) + if (instance?.Process == null) { return Conflict($"Process is not started. Use start!"); } @@ -267,60 +255,48 @@ public async Task> NextElement( return Conflict($"Process is ended."); } - if (!string.IsNullOrEmpty(elementId)) - { - ElementInfo elemInfo = _processReader.GetElementInfo(elementId); - if (elemInfo == null) - { - return BadRequest($"Requested element id {elementId} is not found in process definition"); - } - } - - string altinnTaskType = instance.Process.CurrentTask?.AltinnTaskType; + string? altinnTaskType = instance.Process?.CurrentTask?.AltinnTaskType; if (altinnTaskType == null) { return Conflict($"Instance does not have current altinn task type information!"); } - ProcessSequenceFlowType processSequenceFlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext; - List possibleNextElements = await _flowHydration.NextFollowAndFilterGateways(instance, instance.Process.CurrentTask?.ElementId, elementId.IsNullOrEmpty()); - string targetElement = ProcessHelper.GetValidNextElementOrError(elementId, possibleNextElements.Select(e => e.Id).ToList(), out ProcessError processError); - - if (!string.IsNullOrEmpty(elementId) && processError == null) - { - List flows = _processReader.GetSequenceFlowsBetween(instance.Process.CurrentTask?.ElementId, targetElement); - processSequenceFlowType = ProcessHelper.GetSequenceFlowType(flows); - } - bool authorized; - if (processSequenceFlowType.Equals(ProcessSequenceFlowType.CompleteCurrentMoveToNext)) - { - authorized = await AuthorizeAction(altinnTaskType, org, app, instanceOwnerPartyId, instanceGuid); - } - else - { - ElementInfo elemInfo = _processReader.GetElementInfo(targetElement); - authorized = await AuthorizeAction(elemInfo.AltinnTaskType, org, app, instanceOwnerPartyId, instanceGuid, elemInfo.Id); - } + string? checkedAction = EnsureActionNotTaskType(processNext?.Action ?? altinnTaskType); + authorized = await AuthorizeAction(checkedAction, org, app, instanceOwnerPartyId, instanceGuid, instance.Process?.CurrentTask?.ElementId); if (!authorized) { return Forbid(); } - - ProcessChangeContext changeContext = new ProcessChangeContext(instance, User); - changeContext.RequestedProcessElementId = elementId; - changeContext = await _processEngine.Next(changeContext); - if (changeContext.FailedProcessChange) + + _logger.LogDebug("User is authorized to perform action {Action}", checkedAction); + var request = new ProcessNextRequest() + { + Instance = instance, + User = User, + Action = checkedAction + }; + var result = await _processEngine.Next(request); + if (!result.Success) { - return Conflict(changeContext.ProcessMessages[0].Message); + switch (result.ErrorType) + { + case ProcessErrorType.Conflict: + return Conflict(result.ErrorMessage); + case ProcessErrorType.Internal: + return StatusCode(500, result.ErrorMessage); + } } - return Ok(changeContext.Instance.Process); + AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, result.ProcessStateChange?.NewProcessState); + + return Ok(appProcessState); } catch (PlatformHttpException e) { + _logger.LogError("Platform exception when processing next. {message}", e.Message); return HandlePlatformHttpException(e, "Process next failed."); } catch (Exception exception) @@ -342,7 +318,7 @@ public async Task> NextElement( [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task> CompleteProcess( + public async Task> CompleteProcess( [FromRoute] string org, [FromRoute] string app, [FromRoute] int instanceOwnerPartyId, @@ -382,9 +358,9 @@ public async Task> CompleteProcess( int counter = 0; do { - string altinnTaskType = instance.Process.CurrentTask?.AltinnTaskType; + string altinnTaskType = EnsureActionNotTaskType(instance.Process.CurrentTask.AltinnTaskType); - bool authorized = await AuthorizeAction(altinnTaskType, org, app, instanceOwnerPartyId, instanceGuid); + bool authorized = await AuthorizeAction(altinnTaskType, org, app, instanceOwnerPartyId, instanceGuid, instance.Process.CurrentTask.ElementId); if (!authorized) { return Forbid(); @@ -395,27 +371,33 @@ public async Task> CompleteProcess( return Conflict($"Instance is not valid for task {currentTaskId}. Automatic completion of process is stopped"); } - List nextElements = await _flowHydration.NextFollowAndFilterGateways(instance, currentTaskId); - - if (nextElements.Count > 1) - { - return Conflict($"Cannot complete process. Multiple outgoing sequence flows detected from task {currentTaskId}. Please select manually among {nextElements}"); - } - - string nextElement = nextElements.First().Id; - try { - ProcessChangeContext processChange = new ProcessChangeContext(instance, User); - processChange.RequestedProcessElementId = nextElement; - processChange = await _processEngine.Next(processChange); + ProcessNextRequest request = new ProcessNextRequest() + { + Instance = instance, + User = User, + Action = altinnTaskType + }; + var result = await _processEngine.Next(request); + + if (!result.Success) + { + switch (result.ErrorType) + { + case ProcessErrorType.Conflict: + return Conflict(result.ErrorMessage); + case ProcessErrorType.Internal: + return StatusCode(500, result.ErrorMessage); + } + } - if (processChange.FailedProcessChange) + if (result.ProcessStateChange?.NewProcessState is null) { - return Conflict(processChange.ProcessMessages[0].Message); + return StatusCode(500, "Something is not right"); } - currentTaskId = instance.Process.CurrentTask?.ElementId; + currentTaskId = result.ProcessStateChange.NewProcessState.CurrentTask.ElementId; } catch (Exception ex) { @@ -428,11 +410,12 @@ public async Task> CompleteProcess( if (counter > MaxIterationsAllowed) { - _logger.LogError($"More than {counter} iterations detected in process. Possible loop. Fix app {org}/{app}'s process definition!"); + _logger.LogError($"More than {MaxIterationsAllowed} iterations detected in process. Possible loop. Fix app's process definition!"); return StatusCode(500, $"More than {counter} iterations detected in process. Possible loop. Fix app process definition!"); } - return Ok(instance.Process); + AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, instance.Process); + return Ok(appProcessState); } /// @@ -448,7 +431,7 @@ public async Task GetProcessHistory( { try { - return Ok(await _processService.GetProcessHistory(instanceGuid.ToString(), instanceOwnerPartyId.ToString())); + return Ok(await _processClient.GetProcessHistory(instanceGuid.ToString(), instanceOwnerPartyId.ToString())); } catch (PlatformHttpException e) { @@ -460,53 +443,80 @@ public async Task GetProcessHistory( return ExceptionResponse(processException, $"Unable to find retrieve process history for instance {instanceOwnerPartyId}/{instanceGuid}. Exception: {processException}"); } } + + private async Task ConvertAndAuthorizeActions(Instance instance, ProcessState? processState) + { + AppProcessState appProcessState = new AppProcessState(processState); + if (appProcessState.CurrentTask?.ElementId != null) + { + var flowElement = _processReader.GetFlowElement(appProcessState.CurrentTask.ElementId); + if (flowElement is ProcessTask processTask) + { + appProcessState.CurrentTask.Actions = new Dictionary(); + List actions = new List() { new("read"), new("write") }; + actions.AddRange(processTask.ExtensionElements?.TaskExtension?.AltinnActions ?? new List()); + var authDecisions = await AuthorizeActions(actions, instance); + appProcessState.CurrentTask.Actions = authDecisions.Where(a => a.ActionType == ActionType.ProcessAction).ToDictionary(a => a.Id, a => a.Authorized); + appProcessState.CurrentTask.HasReadAccess = authDecisions.Single(a => a.Id == "read").Authorized; + appProcessState.CurrentTask.HasWriteAccess = authDecisions.Single(a => a.Id == "write").Authorized; + appProcessState.CurrentTask.UserActions = authDecisions; + } + } + + var processTasks = new List(); + foreach (var processElement in _processReader.GetAllFlowElements().OfType()) + { + processTasks.Add(new AppProcessTaskTypeInfo + { + ElementId = processElement.Id, + AltinnTaskType = processElement.ExtensionElements?.TaskExtension?.TaskType + }); + } + + appProcessState.ProcessTasks = processTasks; + + return appProcessState; + } private ActionResult ExceptionResponse(Exception exception, string message) { _logger.LogError(exception, message); - if (exception is PlatformHttpException) + if (exception is PlatformHttpException phe) { - PlatformHttpException phe = exception as PlatformHttpException; return StatusCode((int)phe.Response.StatusCode, phe.Message); } - else if (exception is ServiceException) + else if (exception is ServiceException se) { - ServiceException se = exception as ServiceException; return StatusCode((int)se.StatusCode, se.Message); } return StatusCode(500, $"{message}"); } - private async Task AuthorizeAction(string currentTaskType, string org, string app, int instanceOwnerPartyId, Guid instanceGuid, string taskId = null) + private async Task AuthorizeAction(string action, string org, string app, int instanceOwnerPartyId, Guid instanceGuid, string? taskId = null) + { + return await _authorization.AuthorizeAction(new AppIdentifier(org, app), new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), HttpContext.User, action, taskId); + } + + private async Task> AuthorizeActions(List actions, Instance instance) { - string actionType; + return await _authorization.AuthorizeActions(instance, HttpContext.User, actions); + } - switch (currentTaskType) + private static string EnsureActionNotTaskType(string actionOrTaskType) + { + switch (actionOrTaskType) { case "data": case "feedback": - actionType = "write"; - break; + return "write"; case "confirmation": - actionType = "confirm"; - break; + return "confirm"; default: - actionType = currentTaskType; - break; - } - - XacmlJsonRequestRoot request = DecisionHelper.CreateDecisionRequest(org, app, HttpContext.User, actionType, instanceOwnerPartyId, instanceGuid, taskId); - XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); - if (response?.Response == null) - { - _logger.LogInformation($"// Process Controller // Authorization of moving process forward failed with request: {JsonConvert.SerializeObject(request)}."); - return false; + // Not any known task type, so assume it is an action type + return actionOrTaskType; } - - bool authorized = DecisionHelper.ValidatePdpDecision(response.Response, HttpContext.User); - return authorized; } private ActionResult HandlePlatformHttpException(PlatformHttpException e, string defaultMessage) @@ -515,18 +525,25 @@ private ActionResult HandlePlatformHttpException(PlatformHttpException e, string { return Forbid(); } - else if (e.Response.StatusCode == HttpStatusCode.NotFound) + + if (e.Response.StatusCode == HttpStatusCode.NotFound) { return NotFound(); } - else if (e.Response.StatusCode == HttpStatusCode.Conflict) + + if (e.Response.StatusCode == HttpStatusCode.Conflict) { return Conflict(); } - else - { - return ExceptionResponse(e, defaultMessage); - } + + return ExceptionResponse(e, defaultMessage); + } + + private static async Task DeserializeFromStream(Stream stream) + { + using StreamReader reader = new StreamReader(stream); + string text = await reader.ReadToEndAsync(); + return JsonConvert.DeserializeObject(text); } } } diff --git a/src/Altinn.App.Api/Controllers/ProfileController.cs b/src/Altinn.App.Api/Controllers/ProfileController.cs index b0e91b276..c5d01cc29 100644 --- a/src/Altinn.App.Api/Controllers/ProfileController.cs +++ b/src/Altinn.App.Api/Controllers/ProfileController.cs @@ -1,11 +1,9 @@ -using System; -using System.Threading.Tasks; +#nullable enable + using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Profile; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace Altinn.App.Api.Controllers { @@ -17,17 +15,15 @@ namespace Altinn.App.Api.Controllers [ApiController] public class ProfileController : Controller { - private readonly IProfile _profileClient; - private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IProfileClient _profileClient; private readonly ILogger _logger; /// /// Initializes a new instance of the class /// - public ProfileController(IProfile profileClient, IHttpContextAccessor httpContextAccessor, ILogger logger) + public ProfileController(IProfileClient profileClient, ILogger logger) { _profileClient = profileClient; - _httpContextAccessor = httpContextAccessor; _logger = logger; } @@ -39,7 +35,7 @@ public ProfileController(IProfile profileClient, IHttpContextAccessor httpContex [HttpGet("user")] public async Task GetUser() { - int userId = AuthenticationHelper.GetUserId(_httpContextAccessor.HttpContext); + int userId = AuthenticationHelper.GetUserId(HttpContext); if (userId == 0) { return BadRequest("The userId is not proviced in the context."); diff --git a/src/Altinn.App.Api/Controllers/ResourceController.cs b/src/Altinn.App.Api/Controllers/ResourceController.cs index 10761ef64..7029e3b5e 100644 --- a/src/Altinn.App.Api/Controllers/ResourceController.cs +++ b/src/Altinn.App.Api/Controllers/ResourceController.cs @@ -1,10 +1,8 @@ #nullable enable -using System; using System.Globalization; -using System.IO; using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -267,8 +265,23 @@ public async Task GetFooterLayout(string org, string app) { return NoContent(); } - + return Ok(layout); } + + /// + /// Get validation configuration file. + /// + /// The application owner short name + /// The application name + /// Unique identifier of the model to fetch validations for. + /// The validation configuration file as json. + [HttpGet] + [Route("{org}/{app}/api/validationconfig/{id}")] + public ActionResult GetValidationConfiguration(string org, string app, string id) + { + string? validationConfiguration = _appResourceService.GetValidationConfiguration(id); + return Ok(validationConfiguration); + } } } diff --git a/src/Altinn.App.Api/Controllers/StatelessDataController.cs b/src/Altinn.App.Api/Controllers/StatelessDataController.cs index 07b25e28b..49f458953 100644 --- a/src/Altinn.App.Api/Controllers/StatelessDataController.cs +++ b/src/Altinn.App.Api/Controllers/StatelessDataController.cs @@ -2,13 +2,14 @@ using System.Net; using Altinn.App.Api.Infrastructure.Filters; -using Altinn.App.Api.Mappers; using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Helpers.Serialization; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Prefill; +using Altinn.App.Core.Internal.Registers; using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Helpers; using Altinn.Common.PEP.Interfaces; @@ -32,9 +33,9 @@ public class StatelessDataController : ControllerBase private readonly ILogger _logger; private readonly IAppModel _appModel; private readonly IAppResources _appResourcesService; - private readonly IDataProcessor _dataProcessor; + private readonly IEnumerable _dataProcessors; private readonly IPrefill _prefillService; - private readonly IRegister _registerClient; + private readonly IAltinnPartyClient _altinnPartyClientClient; private readonly IPDP _pdp; private const long REQUEST_SIZE_LIMIT = 2000 * 1024 * 1024; @@ -44,23 +45,23 @@ public class StatelessDataController : ControllerBase private const string OrgPrefix = "org"; /// - /// The stateless data controller is responsible for creating and updating stateles data elements. + /// The stateless data controller is responsible for creating and updating stateless data elements. /// public StatelessDataController( ILogger logger, IAppModel appModel, IAppResources appResourcesService, - IDataProcessor dataProcessor, IPrefill prefillService, - IRegister registerClient, - IPDP pdp) + IAltinnPartyClient altinnPartyClientClient, + IPDP pdp, + IEnumerable dataProcessors) { _logger = logger; _appModel = appModel; _appResourcesService = appResourcesService; - _dataProcessor = dataProcessor; + _dataProcessors = dataProcessors; _prefillService = prefillService; - _registerClient = registerClient; + _altinnPartyClientClient = altinnPartyClientClient; _pdp = pdp; } @@ -114,12 +115,24 @@ public async Task Get( // runs prefill from repo configuration if config exists await _prefillService.PrefillDataModel(owner.PartyId, dataType, appModel); - Instance virutalInstance = new Instance() { InstanceOwner = owner }; - await _dataProcessor.ProcessDataRead(virutalInstance, null, appModel); + Instance virtualInstance = new Instance() { InstanceOwner = owner }; + await ProcessAllDataRead(virtualInstance, appModel); return Ok(appModel); } + private async Task ProcessAllDataRead(Instance virtualInstance, object appModel) + { + foreach (var dataProcessor in _dataProcessors) + { + _logger.LogInformation( + "ProcessDataRead for {modelType} using {dataProcesor}", + appModel.GetType().Name, + dataProcessor.GetType().Name); + await dataProcessor.ProcessDataRead(virtualInstance, null, appModel); + } + } + /// /// Create a new data object of the defined data type /// @@ -147,10 +160,8 @@ public async Task GetAnonymous([FromQuery] string dataType) } object appModel = _appModel.Create(classRef); - - var virutalInstance = new Instance(); - - await _dataProcessor.ProcessDataRead(virutalInstance, null, appModel); + var virtualInstance = new Instance(); + await ProcessAllDataRead(virtualInstance, appModel); return Ok(appModel); } @@ -204,7 +215,7 @@ public async Task Post( ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); object? appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (!string.IsNullOrEmpty(deserializer.Error)) + if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) { return BadRequest(deserializer.Error); } @@ -212,8 +223,12 @@ public async Task Post( // runs prefill from repo configuration if config exists await _prefillService.PrefillDataModel(owner.PartyId, dataType, appModel); - Instance virutalInstance = new Instance() { InstanceOwner = owner }; - await _dataProcessor.ProcessDataRead(virutalInstance, null, appModel); + Instance virtualInstance = new Instance() { InstanceOwner = owner }; + foreach (var dataProcessor in _dataProcessors) + { + _logger.LogInformation("ProcessDataRead for {modelType} using {dataProcesor}", appModel.GetType().Name, dataProcessor.GetType().Name); + await dataProcessor.ProcessDataRead(virtualInstance, null, appModel); + } return Ok(appModel); } @@ -247,23 +262,27 @@ public async Task PostAnonymous([FromQuery] string dataType) ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); object? appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (!string.IsNullOrEmpty(deserializer.Error)) + if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) { return BadRequest(deserializer.Error); } - Instance virutalInstance = new Instance(); - await _dataProcessor.ProcessDataRead(virutalInstance, null, appModel); + Instance virtualInstance = new Instance(); + foreach (var dataProcessor in _dataProcessors) + { + _logger.LogInformation("ProcessDataRead for {modelType} using {dataProcesor}", appModel.GetType().Name, dataProcessor.GetType().Name); + await dataProcessor.ProcessDataRead(virtualInstance, null, appModel); + } return Ok(appModel); } - private async Task GetInstanceOwner(string partyFromHeader) + private async Task GetInstanceOwner(string? partyFromHeader) { // Use the party id of the logged in user, if no party id is given in the header - // Not sure if this is really used anywhere. It doesn't seem usefull, as you'd + // Not sure if this is really used anywhere. It doesn't seem useful, as you'd // always want to create an instance based on the selected party, not the person - // you happend to log in as. + // you happened to log in as. if (partyFromHeader is null) { var partyId = Request.HttpContext.User.GetPartyIdAsInt(); @@ -272,7 +291,7 @@ public async Task PostAnonymous([FromQuery] string dataType) return null; } - var partyFromUser = await _registerClient.GetParty(partyId.Value); + var partyFromUser = await _altinnPartyClientClient.GetParty(partyId.Value); if (partyFromUser is null) { return null; @@ -292,11 +311,11 @@ public async Task PostAnonymous([FromQuery] string dataType) var idPrefix = headerParts[0].ToLowerInvariant(); var party = idPrefix switch { - PartyPrefix => await _registerClient.GetParty(int.TryParse(id, out var partyId) ? partyId : 0), + PartyPrefix => await _altinnPartyClientClient.GetParty(int.TryParse(id, out var partyId) ? partyId : 0), // Frontend seems to only use partyId, not orgnr or ssn. - PersonPrefix => await _registerClient.LookupParty(new PartyLookup { Ssn = id }), - OrgPrefix => await _registerClient.LookupParty(new PartyLookup { OrgNo = id }), + PersonPrefix => await _altinnPartyClientClient.LookupParty(new PartyLookup { Ssn = id }), + OrgPrefix => await _altinnPartyClientClient.LookupParty(new PartyLookup { OrgNo = id }), _ => null, }; diff --git a/src/Altinn.App.Api/Controllers/StatelessPagesController.cs b/src/Altinn.App.Api/Controllers/StatelessPagesController.cs index c57e74a1d..f291cbcbb 100644 --- a/src/Altinn.App.Api/Controllers/StatelessPagesController.cs +++ b/src/Altinn.App.Api/Controllers/StatelessPagesController.cs @@ -1,7 +1,5 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Altinn.App.Core.Features; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Features; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; using Altinn.App.Core.Models; using Microsoft.AspNetCore.Authorization; @@ -17,6 +15,7 @@ namespace Altinn.App.Api.Controllers [ApiController] [Route("{org}/{app}/v1/pages")] [AllowAnonymous] + [Obsolete("IPageOrder does not work with frontend version 4")] public class StatelessPagesController : ControllerBase { private readonly IAppModel _appModel; diff --git a/src/Altinn.App.Api/Controllers/TextsController.cs b/src/Altinn.App.Api/Controllers/TextsController.cs index c2b2e3d4a..818a9e118 100644 --- a/src/Altinn.App.Api/Controllers/TextsController.cs +++ b/src/Altinn.App.Api/Controllers/TextsController.cs @@ -1,5 +1,6 @@ -using System.Threading.Tasks; -using Altinn.App.Core.Interface; +#nullable enable + +using Altinn.App.Core.Internal.App; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Mvc; @@ -38,7 +39,7 @@ public async Task> Get(string org, string app, [FromR return BadRequest($"Provided language {language} is invalid. Language code should consists of two characters."); } - TextResource textResource = await _appResources.GetTexts(org, app, language); + TextResource? textResource = await _appResources.GetTexts(org, app, language); if (textResource == null && language != "nb") { diff --git a/src/Altinn.App.Api/Controllers/ValidateController.cs b/src/Altinn.App.Api/Controllers/ValidateController.cs index f0db0bad9..5646edeef 100644 --- a/src/Altinn.App.Api/Controllers/ValidateController.cs +++ b/src/Altinn.App.Api/Controllers/ValidateController.cs @@ -1,7 +1,10 @@ +#nullable enable + +using Altinn.App.Core.Features; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Authorization; @@ -16,16 +19,16 @@ namespace Altinn.App.Api.Controllers [ApiController] public class ValidateController : ControllerBase { - private readonly IInstance _instanceClient; + private readonly IInstanceClient _instanceClient; private readonly IAppMetadata _appMetadata; - private readonly IValidation _validationService; + private readonly IValidationService _validationService; /// /// Initialises a new instance of the class /// public ValidateController( - IInstance instanceClient, - IValidation validationService, + IInstanceClient instanceClient, + IValidationService validationService, IAppMetadata appMetadata) { _instanceClient = instanceClient; @@ -49,13 +52,13 @@ public async Task ValidateInstance( [FromRoute] int instanceOwnerPartyId, [FromRoute] Guid instanceGuid) { - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + Instance? instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); if (instance == null) { return NotFound(); } - string taskId = instance.Process?.CurrentTask?.ElementId; + string? taskId = instance.Process?.CurrentTask?.ElementId; if (taskId == null) { throw new ValidationException("Unable to validate instance without a started process."); @@ -63,7 +66,7 @@ public async Task ValidateInstance( try { - List messages = await _validationService.ValidateAndUpdateProcess(instance, taskId); + List messages = await _validationService.ValidateInstanceAtTask(instance, taskId); return Ok(messages); } catch (PlatformHttpException exception) @@ -78,8 +81,7 @@ public async Task ValidateInstance( } /// - /// Validate an app instance. This will validate all individual data elements, both the binary elements and the elements bound - /// to a model, and then finally the state of the instance. + /// Validate an app instance. This will validate a single data element /// /// Unique identifier of the organisation responsible for the app. /// Application identifier which is unique within an organisation @@ -95,15 +97,12 @@ public async Task ValidateData( [FromRoute] Guid instanceId, [FromRoute] Guid dataGuid) { - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerId, instanceId); + Instance? instance = await _instanceClient.GetInstance(app, org, instanceOwnerId, instanceId); if (instance == null) { return NotFound(); } - // Todo. Figure out where to get this from - Dictionary> serviceText = new Dictionary>(); - if (instance.Process?.CurrentTask?.ElementId == null) { throw new ValidationException("Unable to validate instance without a started process."); @@ -111,7 +110,7 @@ public async Task ValidateData( List messages = new List(); - DataElement element = instance.Data.FirstOrDefault(d => d.Id == dataGuid.ToString()); + DataElement? element = instance.Data.FirstOrDefault(d => d.Id == dataGuid.ToString()); if (element == null) { @@ -120,26 +119,28 @@ public async Task ValidateData( Application application = await _appMetadata.GetApplicationMetadata(); - DataType dataType = application.DataTypes.FirstOrDefault(et => et.Id == element.DataType); + DataType? dataType = application.DataTypes.FirstOrDefault(et => et.Id == element.DataType); if (dataType == null) { throw new ValidationException("Unknown element type."); } - messages.AddRange(await _validationService.ValidateDataElement(instance, dataType, element)); + messages.AddRange(await _validationService.ValidateDataElement(instance, element, dataType)); string taskId = instance.Process.CurrentTask.ElementId; + + // Should this be a BadRequest instead? if (!dataType.TaskId.Equals(taskId, StringComparison.OrdinalIgnoreCase)) { ValidationIssue message = new ValidationIssue { Code = ValidationIssueCodes.DataElementCodes.DataElementValidatedAtWrongTask, - InstanceId = instance.Id, Severity = ValidationIssueSeverity.Warning, DataElementId = element.Id, - Description = AppTextHelper.GetAppText( - ValidationIssueCodes.DataElementCodes.DataElementValidatedAtWrongTask, serviceText, null, "nb") + Description = $"Data element for task {dataType.TaskId} validated while currentTask is {taskId}", + CustomTextKey = ValidationIssueCodes.DataElementCodes.DataElementValidatedAtWrongTask, + CustomTextParams = new List() { dataType.TaskId, taskId }, }; messages.Add(message); } diff --git a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs index f541e6d79..9c1bbddf4 100644 --- a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable +using System; +using Altinn.App.Api.Configuration; using Altinn.App.Api.Controllers; using Altinn.App.Api.Infrastructure.Filters; using Altinn.App.Api.Infrastructure.Health; @@ -18,6 +20,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.FeatureManagement; using Microsoft.IdentityModel.Tokens; +using Prometheus; namespace Altinn.App.Api.Extensions { @@ -74,6 +77,7 @@ public static void AddAltinnAppServices(this IServiceCollection services, IConfi services.AddHttpClient(); services.AddSingleton(); + services.AddMetricsServer(config); } private static void AddApplicationInsights(IServiceCollection services, IConfiguration config, IWebHostEnvironment env) @@ -160,5 +164,15 @@ private static void AddAntiforgery(IServiceCollection services) services.TryAddSingleton(); } + + private static void AddMetricsServer(this IServiceCollection services, IConfiguration config) + { + var metricsSettings = config.GetSection("MetricsSettings").Get() ?? new MetricsSettings(); + if (metricsSettings.Enabled) + { + ushort port = metricsSettings.Port; + services.AddMetricServer(options => { options.Port = port; }); + } + } } -} \ No newline at end of file +} diff --git a/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs b/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs new file mode 100644 index 000000000..d6fb4613d --- /dev/null +++ b/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs @@ -0,0 +1,60 @@ +#nullable enable +using System.Reflection; +using Altinn.App.Api.Configuration; +using Altinn.App.Api.Helpers; +using Prometheus; + +namespace Altinn.App.Api.Extensions; + +/// +/// Altinn specific extensions for . +/// +public static class WebApplicationBuilderExtensions +{ + /// + /// Add default Altinn configuration for an app. + /// + /// The . + /// + public static IApplicationBuilder UseAltinnAppCommonConfiguration(this IApplicationBuilder app) + { + var appId = StartupHelper.GetApplicationId(); + if (app is WebApplication webApp && webApp.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + webApp.UseAltinnPrometheus(appId); + } + + app.UseHttpMetrics(); + app.UseMetricServer(); + app.UseDefaultSecurityHeaders(); + app.UseRouting(); + app.UseStaticFiles('/' + appId); + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + app.UseHealthChecks("/health"); + return app; + } + + private static void UseAltinnPrometheus(this WebApplication webApp, string appId) + { + var metricsSettings = webApp.Configuration.GetSection("MetricsSettings")?.Get() ?? new MetricsSettings(); + if (!metricsSettings.Enabled) + { + return; + } + + webApp.UseHttpMetrics(); + var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; + Metrics.DefaultRegistry.SetStaticLabels(new Dictionary() + { + { "application_id", appId }, + { "nuget_package_version", version } + }); + } +} diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs b/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs index ff1d8fddd..63c7d29d2 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; +#nullable enable + using Altinn.App.Core.Helpers.Extensions; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Net.Http.Headers; @@ -62,15 +59,13 @@ public async Task Read() { MultipartReader reader = new MultipartReader(GetBoundary(), request.Body); - MultipartSection section; + MultipartSection? section; while ((section = await reader.ReadNextSectionAsync()) != null) { partCounter++; - bool hasContentDispositionHeader = ContentDispositionHeaderValue - .TryParse(section.ContentDisposition, out ContentDispositionHeaderValue contentDisposition); - - if (!hasContentDispositionHeader) + if (!ContentDispositionHeaderValue + .TryParse(section.ContentDisposition, out ContentDispositionHeaderValue? contentDisposition)) { Errors.Add(string.Format("Part number {0} doesn't have a content disposition", partCounter)); continue; @@ -82,8 +77,8 @@ public async Task Read() continue; } - string sectionName = contentDisposition.Name.HasValue ? contentDisposition.Name.Value : null; - string contentFileName = null; + string? sectionName = contentDisposition.Name.Value; + string? contentFileName = null; if (contentDisposition.FileNameStar.HasValue) { contentFileName = contentDisposition.FileNameStar.Value; @@ -135,7 +130,7 @@ public async Task Read() private string GetBoundary() { MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse(request.ContentType); - return mediaType.Boundary.Value.Trim('"'); + return mediaType.Boundary.Value!.Trim('"'); } } } diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs index ea5faf378..1f92dd925 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs @@ -1,4 +1,4 @@ -using System.IO; +#nullable enable namespace Altinn.App.Api.Helpers.RequestHandling { @@ -10,25 +10,25 @@ public class RequestPart /// /// The stream to access this part. /// - public Stream Stream { get; set; } + public Stream Stream { get; set; } = default!; /// /// The file name as given in content description. /// - public string FileName { get; set; } + public string? FileName { get; set; } /// /// The parts name. /// - public string Name { get; set; } + public string? Name { get; set; } /// /// The content type of the part. /// - public string ContentType { get; set; } + public string ContentType { get; set; } = default!; /// - /// The file size of the part, if given. + /// The file size of the part, 0 if not given. /// public long FileSize { get; set; } } diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs index 9ba71fc67..fc1ead428 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Linq; using Altinn.Platform.Storage.Interface.Models; @@ -26,7 +28,7 @@ public RequestPartValidator(Application appInfo) /// /// The request part to be validated. /// null if no errors where found. Otherwise an error message. - public string ValidatePart(RequestPart part) + public string? ValidatePart(RequestPart part) { if (part.Name == "instance") { @@ -39,15 +41,7 @@ public string ValidatePart(RequestPart part) } else { - Console.WriteLine($"// {DateTime.Now} // Debug // Part : {part}"); - Console.WriteLine($"// {DateTime.Now} // Debug // Part name: {part.Name}"); - Console.WriteLine($"// {DateTime.Now} // Debug // appinfo : {appInfo}"); - Console.WriteLine($"// {DateTime.Now} // Debug // appinfo.Id : {appInfo.Id}"); - - DataType dataType = appInfo.DataTypes.Find(e => e.Id == part.Name); - - Console.WriteLine($"// {DateTime.Now} // Debug // elementType : {dataType}"); - + DataType? dataType = appInfo.DataTypes.Find(e => e.Id == part.Name); if (dataType == null) { return $"Multipart section named, '{part.Name}' does not correspond to an element type in application metadata"; @@ -91,11 +85,11 @@ public string ValidatePart(RequestPart part) /// /// The list of request parts to be validated. /// null if no errors where found. Otherwise an error message. - public string ValidateParts(List parts) + public string? ValidateParts(List parts) { foreach (RequestPart part in parts) { - string partError = ValidatePart(part); + string? partError = ValidatePart(part); if (partError != null) { return partError; diff --git a/src/Altinn.App.Api/Helpers/StartupHelper.cs b/src/Altinn.App.Api/Helpers/StartupHelper.cs index 9bac3be5f..6ef8a00ea 100644 --- a/src/Altinn.App.Api/Helpers/StartupHelper.cs +++ b/src/Altinn.App.Api/Helpers/StartupHelper.cs @@ -43,6 +43,6 @@ public static string GetApplicationId() { string appMetaDataString = File.ReadAllText("config/applicationmetadata.json"); JObject appMetadataJObject = JObject.Parse(appMetaDataString); - return appMetadataJObject.SelectToken("id").Value(); + return appMetadataJObject.SelectToken("id")?.Value() ?? throw new Exception("config/applicationmetadata.json does not contain an \"id\" property"); } } \ No newline at end of file diff --git a/src/Altinn.App.Api/Models/AppProcessState.cs b/src/Altinn.App.Api/Models/AppProcessState.cs new file mode 100644 index 000000000..afc1ec4f5 --- /dev/null +++ b/src/Altinn.App.Api/Models/AppProcessState.cs @@ -0,0 +1,16 @@ +#nullable enable +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Api.Models; + +/// +/// Extended representation of a status object that holds the process state of an application instance. +/// The process is defined by the application's process specification BPMN file. +/// +public class AppProcessState: ProcessState +{ + /// + /// Actions that can be performed and if the user is allowed to perform them. + /// + public Dictionary? Actions { get; set; } +} diff --git a/src/Altinn.App.Api/Models/DataPatchRequest.cs b/src/Altinn.App.Api/Models/DataPatchRequest.cs new file mode 100644 index 000000000..74a4f7990 --- /dev/null +++ b/src/Altinn.App.Api/Models/DataPatchRequest.cs @@ -0,0 +1,25 @@ +#nullable enable +using System.Text.Json.Serialization; +using Altinn.App.Api.Controllers; +using Json.Patch; + +namespace Altinn.App.Api.Models; + +/// +/// Represents the request to patch data on the . +/// +public class DataPatchRequest +{ + /// + /// The Patch operation to perform. + /// + [JsonPropertyName("patch")] + public required JsonPatch Patch { get; init; } + + /// + /// List of validators to ignore during the patch operation. + /// Issues from these validators will not be run during the save operation, but the validator will run on process/next + /// + [JsonPropertyName("ignoredValidators")] + public required List? IgnoredValidators { get; init; } +} \ No newline at end of file diff --git a/src/Altinn.App.Api/Models/DataPatchResponse.cs b/src/Altinn.App.Api/Models/DataPatchResponse.cs new file mode 100644 index 000000000..cab836e29 --- /dev/null +++ b/src/Altinn.App.Api/Models/DataPatchResponse.cs @@ -0,0 +1,20 @@ +using Altinn.App.Api.Controllers; +using Altinn.App.Core.Models.Validation; + +namespace Altinn.App.Api.Models; + +/// +/// Represents the response from a data patch operation on the . +/// +public class DataPatchResponse +{ + /// + /// The validation issues that were found during the patch operation. + /// + public required Dictionary> ValidationIssues { get; init; } + + /// + /// The current data model after the patch operation. + /// + public required object NewDataModel { get; init; } +} \ No newline at end of file diff --git a/src/Altinn.App.Api/Models/ProcessNext.cs b/src/Altinn.App.Api/Models/ProcessNext.cs new file mode 100644 index 000000000..cfa277a87 --- /dev/null +++ b/src/Altinn.App.Api/Models/ProcessNext.cs @@ -0,0 +1,16 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.App.Api.Models; + +/// +/// Model for process next body +/// +public class ProcessNext +{ + /// + /// Action performed + /// + [JsonPropertyName("action")] + public string? Action { get; set; } +} \ No newline at end of file diff --git a/src/Altinn.App.Api/Models/UserActionRequest.cs b/src/Altinn.App.Api/Models/UserActionRequest.cs new file mode 100644 index 000000000..d94ce7c16 --- /dev/null +++ b/src/Altinn.App.Api/Models/UserActionRequest.cs @@ -0,0 +1,28 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.App.Api.Models; + +/// +/// Request model for user action +/// +public class UserActionRequest +{ + /// + /// Action performed + /// + [JsonPropertyName("action")] + public string? Action { get; set; } + + /// + /// The id of the button that was clicked + /// + [JsonPropertyName("buttonId")] + public string? ButtonId { get; set; } + + /// + /// Additional metadata for the action + /// + [JsonPropertyName("metadata")] + public Dictionary? Metadata { get; set; } +} \ No newline at end of file diff --git a/src/Altinn.App.Api/Models/UserActionResponse.cs b/src/Altinn.App.Api/Models/UserActionResponse.cs new file mode 100644 index 000000000..ee5b315f1 --- /dev/null +++ b/src/Altinn.App.Api/Models/UserActionResponse.cs @@ -0,0 +1,29 @@ +#nullable enable +using System.Text.Json.Serialization; +using Altinn.App.Core.Models.UserAction; + +namespace Altinn.App.Api.Models; + +/// +/// Response object from action endpoint +/// +public class UserActionResponse +{ + /// + /// Data models that have been updated + /// + [JsonPropertyName("updatedDataModels")] + public Dictionary? UpdatedDataModels { get; set; } + + /// + /// Actions the client should perform after action has been performed backend + /// + [JsonPropertyName("clientActions")] + public List? ClientActions { get; set; } + + /// + /// Validation issues that occured when processing action + /// + [JsonPropertyName("error")] + public ActionError? Error { get; set; } +} diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index 1bda5bbc7..d2e10c84f 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -1,6 +1,6 @@ - + - net6.0 + net8.0 enable enable @@ -15,16 +15,18 @@ + + + - diff --git a/src/Altinn.App.Core/Configuration/AppSettings.cs b/src/Altinn.App.Core/Configuration/AppSettings.cs index 9974f7960..ab34f341d 100644 --- a/src/Altinn.App.Core/Configuration/AppSettings.cs +++ b/src/Altinn.App.Core/Configuration/AppSettings.cs @@ -23,6 +23,11 @@ public class AppSettings /// public const string JSON_SCHEMA_FILENAME = "schema.json"; + /// + /// Constant for the location of validation configuration file + /// + public const string VALIDATION_CONFIG_FILENAME = "validation.json"; + /// /// The app configuration baseUrl where files are stored in the container /// @@ -83,7 +88,7 @@ public class AppSettings /// public string LayoutSetsFileName { get; set; } = "layout-sets.json"; - /// + /// /// Gets or sets the name of the layout setting file name /// public string FooterFileName { get; set; } = "footer.json"; @@ -103,6 +108,11 @@ public class AppSettings /// public string JsonSchemaFileName { get; set; } = JSON_SCHEMA_FILENAME; + /// + /// Gets or sets The JSON schema file name + /// + public string ValidationConfigurationFileName { get; set; } = VALIDATION_CONFIG_FILENAME; + /// /// Gets or sets the filename for application meta data /// @@ -214,8 +224,18 @@ public string GetResourceFolder() public string AppVersion { get; set; } /// - /// Enable the preview functionality to load layout in backend and remove data from hidden components before validation and task completion + /// Enable the functionality to load layout in backend and remove data from hidden components before task completion + /// + public bool RemoveHiddenData { get; set; } = false; + + /// + /// Enable the functionality to load layout in backend and validate required fields as defined in the layout + /// + public bool RequiredValidation { get; set; } = false; + + /// + /// Enable the functionality to run expression validation in backend /// - public bool RemoveHiddenDataPreview { get; set; } = false; + public bool ExpressionValidation { get; set; } = false; } } diff --git a/src/Altinn.App.Core/Configuration/MetricsSettings.cs b/src/Altinn.App.Core/Configuration/MetricsSettings.cs new file mode 100644 index 000000000..3de05e3df --- /dev/null +++ b/src/Altinn.App.Core/Configuration/MetricsSettings.cs @@ -0,0 +1,16 @@ +namespace Altinn.App.Api.Configuration; + +/// +/// Metric settings for Altinn Apps +/// +public class MetricsSettings +{ + /// + /// Gets or sets a value indicating whether metrics is enabled or not + /// + public bool Enabled { get; set; } = true; + /// + /// Gets or sets the port for the metrics server is exposed + /// + public ushort Port { get; set; } = 5006; +} diff --git a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs index b246fae4b..ef4e291b1 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs @@ -1,15 +1,15 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.EFormidling.Interface; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Models; using Altinn.Common.AccessTokenClient.Services; using Altinn.Common.EFormidlingClient; using Altinn.Common.EFormidlingClient.Models.SBD; using Altinn.Platform.Storage.Interface.Models; -using AltinnCore.Authentication.Utils; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -22,26 +22,26 @@ public class DefaultEFormidlingService : IEFormidlingService { private readonly ILogger _logger; private readonly IAccessTokenGenerator? _tokenGenerator; - private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IUserTokenProvider _userTokenProvider; private readonly AppSettings? _appSettings; private readonly PlatformSettings? _platformSettings; private readonly IEFormidlingClient? _eFormidlingClient; private readonly IEFormidlingMetadata? _eFormidlingMetadata; private readonly IAppMetadata _appMetadata; - private readonly IData _dataClient; + private readonly IDataClient _dataClient; private readonly IEFormidlingReceivers _eFormidlingReceivers; - private readonly IEvents _eventClient; + private readonly IEventsClient _eventClient; /// /// Initializes a new instance of the class. /// public DefaultEFormidlingService( ILogger logger, - IHttpContextAccessor httpContextAccessor, + IUserTokenProvider userTokenProvider, IAppMetadata appMetadata, - IData dataClient, + IDataClient dataClient, IEFormidlingReceivers eFormidlingReceivers, - IEvents eventClient, + IEventsClient eventClient, IOptions? appSettings = null, IOptions? platformSettings = null, IEFormidlingClient? eFormidlingClient = null, @@ -50,9 +50,9 @@ public DefaultEFormidlingService( { _logger = logger; _tokenGenerator = tokenGenerator; - _httpContextAccessor = httpContextAccessor; _appSettings = appSettings?.Value; _platformSettings = platformSettings?.Value; + _userTokenProvider = userTokenProvider; _eFormidlingClient = eFormidlingClient; _eFormidlingMetadata = eFormidlingMetadata; _appMetadata = appMetadata; @@ -75,7 +75,7 @@ public async Task SendEFormidlingShipment(Instance instance) ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); string accessToken = _tokenGenerator.GenerateAccessToken(applicationMetadata.Org, applicationMetadata.AppIdentifier.App); - string authzToken = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _appSettings.RuntimeCookieName); + string authzToken = _userTokenProvider.GetUserToken(); var requestHeaders = new Dictionary { @@ -113,7 +113,7 @@ public async Task SendEFormidlingShipment(Instance instance) private async Task ConstructStandardBusinessDocument(string instanceGuid, Instance instance) { - DateTime completedTime = DateTime.Now; + DateTime completedTime = DateTime.UtcNow; Sender digdirSender = new Sender { diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs index 5c4c77aa6..c4b57df12 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs @@ -4,7 +4,6 @@ /// Exception thrown when Eformidling is unable to process the message delivered to /// the integration point. /// - [Serializable] public class EformidlingDeliveryException : Exception { /// @@ -23,11 +22,5 @@ public EformidlingDeliveryException(string message, Exception inner) : base(message, inner) { } - - /// - protected EformidlingDeliveryException(System.Runtime.Serialization.SerializationInfo serializationInfo, System.Runtime.Serialization.StreamingContext streamingContext) - : base(serializationInfo, streamingContext) - { - } } } diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs index 9027b1a0c..b9fcdcc9d 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs @@ -8,19 +8,15 @@ using Altinn.App.Core.Helpers; using Altinn.App.Core.Infrastructure.Clients.Maskinporten; using Altinn.App.Core.Infrastructure.Clients.Storage; -using Altinn.App.Core.Interface; using Altinn.App.Core.Models; using Altinn.Common.EFormidlingClient; using Altinn.Common.EFormidlingClient.Models; using Altinn.Platform.Storage.Interface.Models; -using AltinnCore.Authentication.Utils; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; -using System; using System.Net; using System.Net.Http.Headers; -using System.Runtime; using System.Security.Cryptography.X509Certificates; namespace Altinn.App.Core.EFormidling.Implementation diff --git a/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs b/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs index 310dbb52a..411cfc5c1 100644 --- a/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs +++ b/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Net; +using System.Text; namespace Altinn.App.Core.Extensions { @@ -8,9 +9,9 @@ namespace Altinn.App.Core.Extensions public static class DictionaryExtensions { /// - /// Converts a dictionary to a name value string on the form key1=value1,key2=value2 + /// Converts a dictionary to a name value string on the form key1=value1,key2=value2 url encoding both key and value. /// - public static string ToNameValueString(this Dictionary parameters, char separator) + public static string ToUrlEncodedNameValueString(this Dictionary? parameters, char separator) { if (parameters == null) { @@ -24,8 +25,12 @@ public static string ToNameValueString(this Dictionary parameter { builder.Append(separator); } - builder.Append(param.Key + "=" + param.Value); + + builder.Append(WebUtility.UrlEncode(param.Key)); + builder.Append('='); + builder.Append(WebUtility.UrlEncode(param.Value)); } + return builder.ToString(); } } diff --git a/src/Altinn.App.Core/Extensions/HttpClientExtension.cs b/src/Altinn.App.Core/Extensions/HttpClientExtension.cs index accfa7989..2b892c84d 100644 --- a/src/Altinn.App.Core/Extensions/HttpClientExtension.cs +++ b/src/Altinn.App.Core/Extensions/HttpClientExtension.cs @@ -14,7 +14,7 @@ public static class HttpClientExtension /// The http content /// The platformAccess tokens /// A HttpResponseMessage - public static Task PostAsync(this HttpClient httpClient, string authorizationToken, string requestUri, HttpContent content, string? platformAccessToken = null) + public static Task PostAsync(this HttpClient httpClient, string authorizationToken, string requestUri, HttpContent? content, string? platformAccessToken = null) { HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); request.Headers.Add("Authorization", "Bearer " + authorizationToken); @@ -36,7 +36,7 @@ public static Task PostAsync(this HttpClient httpClient, st /// The http content /// The platformAccess tokens /// A HttpResponseMessage - public static Task PutAsync(this HttpClient httpClient, string authorizationToken, string requestUri, HttpContent content, string? platformAccessToken = null) + public static Task PutAsync(this HttpClient httpClient, string authorizationToken, string requestUri, HttpContent? content, string? platformAccessToken = null) { HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, requestUri); request.Headers.Add("Authorization", "Bearer " + authorizationToken); diff --git a/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs b/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs index 8ba739d8b..938ca08f4 100644 --- a/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs +++ b/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs @@ -15,7 +15,7 @@ public static class HttpContextExtensions public static StreamContent CreateContentStream(this HttpRequest request) { StreamContent content = new StreamContent(request.Body); - content.Headers.ContentType = MediaTypeHeaderValue.Parse(request.ContentType); + content.Headers.ContentType = MediaTypeHeaderValue.Parse(request.ContentType!); if (request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues)) { diff --git a/src/Altinn.App.Core/Extensions/InstanceEventExtensions.cs b/src/Altinn.App.Core/Extensions/InstanceEventExtensions.cs new file mode 100644 index 000000000..bc6a54bc9 --- /dev/null +++ b/src/Altinn.App.Core/Extensions/InstanceEventExtensions.cs @@ -0,0 +1,55 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Extensions; + +/// +/// Extension methods for . +/// +public static class InstanceEventExtensions +{ + /// + /// Copies the values of the original to a new instance. + /// + /// The original . + /// New object with copies of values form original + public static InstanceEvent CopyValues(this InstanceEvent original) + { + return new InstanceEvent + { + Created = original.Created, + DataId = original.DataId, + EventType = original.EventType, + Id = original.Id, + InstanceId = original.InstanceId, + InstanceOwnerPartyId = original.InstanceOwnerPartyId, + ProcessInfo = new ProcessState + { + Started = original.ProcessInfo?.Started, + CurrentTask = new ProcessElementInfo + { + Flow = original.ProcessInfo?.CurrentTask?.Flow, + AltinnTaskType = original.ProcessInfo?.CurrentTask?.AltinnTaskType, + ElementId = original.ProcessInfo?.CurrentTask?.ElementId, + Name = original.ProcessInfo?.CurrentTask?.Name, + Started = original.ProcessInfo?.CurrentTask?.Started, + Ended = original.ProcessInfo?.CurrentTask?.Ended, + Validated = new ValidationStatus + { + CanCompleteTask = original.ProcessInfo?.CurrentTask?.Validated?.CanCompleteTask ?? false, + Timestamp = original.ProcessInfo?.CurrentTask?.Validated?.Timestamp + } + }, + + StartEvent = original.ProcessInfo?.StartEvent + }, + User = new PlatformUser + { + AuthenticationLevel = original.User.AuthenticationLevel, + EndUserSystemId = original.User.EndUserSystemId, + OrgId = original.User.OrgId, + UserId = original.User.UserId, + NationalIdentityNumber = original.User?.NationalIdentityNumber + } + }; + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Extensions/ProcessStateExtensions.cs b/src/Altinn.App.Core/Extensions/ProcessStateExtensions.cs new file mode 100644 index 000000000..561e97505 --- /dev/null +++ b/src/Altinn.App.Core/Extensions/ProcessStateExtensions.cs @@ -0,0 +1,39 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Extensions; + +/// +/// Extension methods for +/// +public static class ProcessStateExtensions +{ + /// + /// Copies the values of the original to a new instance. + /// + /// The original . + /// New object with copies of values form original + public static ProcessState Copy(this ProcessState original) + { + ProcessState copyOfState = new ProcessState(); + + if (original.CurrentTask != null) + { + copyOfState.CurrentTask = new ProcessElementInfo(); + copyOfState.CurrentTask.FlowType = original.CurrentTask.FlowType; + copyOfState.CurrentTask.Name = original.CurrentTask.Name; + copyOfState.CurrentTask.Validated = original.CurrentTask.Validated; + copyOfState.CurrentTask.AltinnTaskType = original.CurrentTask.AltinnTaskType; + copyOfState.CurrentTask.Flow = original.CurrentTask.Flow; + copyOfState.CurrentTask.ElementId = original.CurrentTask.ElementId; + copyOfState.CurrentTask.Started = original.CurrentTask.Started; + copyOfState.CurrentTask.Ended = original.CurrentTask.Ended; + } + + copyOfState.EndEvent = original.EndEvent; + copyOfState.Started = original.Started; + copyOfState.Ended = original.Ended; + copyOfState.StartEvent = original.StartEvent; + + return copyOfState; + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index 0c6e01036..67bc5f683 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ +using Altinn.App.Api.Configuration; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Action; using Altinn.App.Core.Features.DataLists; using Altinn.App.Core.Features.DataProcessing; using Altinn.App.Core.Features.FileAnalyzis; @@ -7,6 +9,7 @@ using Altinn.App.Core.Features.PageOrder; using Altinn.App.Core.Features.Pdf; using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Features.Validation.Default; using Altinn.App.Core.Implementation; using Altinn.App.Core.Infrastructure.Clients.Authentication; using Altinn.App.Core.Infrastructure.Clients.Authorization; @@ -16,14 +19,22 @@ using Altinn.App.Core.Infrastructure.Clients.Profile; using Altinn.App.Core.Infrastructure.Clients.Register; using Altinn.App.Core.Infrastructure.Clients.Storage; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Language; using Altinn.App.Core.Internal.Pdf; +using Altinn.App.Core.Internal.Prefill; using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Action; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; +using Altinn.App.Core.Internal.Secrets; +using Altinn.App.Core.Internal.Sign; using Altinn.App.Core.Internal.Texts; using Altinn.App.Core.Models; using Altinn.Common.AccessTokenClient.Configuration; @@ -37,6 +48,11 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Newtonsoft.Json.Linq; +using Prometheus; +using IProcessEngine = Altinn.App.Core.Internal.Process.IProcessEngine; +using IProcessReader = Altinn.App.Core.Internal.Process.IProcessReader; +using ProcessEngine = Altinn.App.Core.Internal.Process.ProcessEngine; +using ProcessReader = Altinn.App.Core.Internal.Process.ProcessReader; namespace Altinn.App.Core.Extensions { @@ -60,27 +76,28 @@ public static void AddPlatformServices(this IServiceCollection services, IConfig AddApplicationIdentifier(services); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); services.AddHttpClient(); - services.AddHttpClient(); - services.Decorate(); - services.AddHttpClient(); + services.AddHttpClient(); + services.Decorate(); + services.AddHttpClient(); +#pragma warning disable CS0618 // Type or member is obsolete services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); +#pragma warning restore CS0618 // Type or member is obsolete + services.AddHttpClient(); + services.AddHttpClient(); services.TryAddTransient(); services.TryAddTransient(); - services.TryAddTransient(); services.TryAddTransient(); + services.TryAddTransient(); } private static void AddApplicationIdentifier(IServiceCollection services) @@ -117,7 +134,7 @@ public static void AddAppServices(this IServiceCollection services, IConfigurati { // Services for Altinn App services.TryAddTransient(); - services.TryAddTransient(); + AddValidationServices(services, configuration); services.TryAddTransient(); services.TryAddTransient(); services.TryAddSingleton(); @@ -125,11 +142,11 @@ public static void AddAppServices(this IServiceCollection services, IConfigurati services.TryAddSingleton(); services.TryAddTransient(); services.TryAddTransient(); +#pragma warning disable CS0618, CS0612 // Type or member is obsolete services.TryAddTransient(); +#pragma warning restore CS0618, CS0612 // Type or member is obsolete services.TryAddTransient(); services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); @@ -142,23 +159,44 @@ public static void AddAppServices(this IServiceCollection services, IConfigurati services.Configure(configuration.GetSection(nameof(FrontEndSettings))); services.Configure(configuration.GetSection(nameof(PdfGeneratorSettings))); AddAppOptions(services); + AddActionServices(services); AddPdfServices(services); AddEventServices(services); AddProcessServices(services); AddFileAnalyserServices(services); AddFileValidatorServices(services); + AddMetricsDecorators(services, configuration); if (!env.IsDevelopment()) { - services.TryAddSingleton(); + services.TryAddSingleton(); services.Configure(configuration.GetSection("kvSetting")); } else { - services.TryAddSingleton(); + services.TryAddSingleton(); } } + private static void AddValidationServices(IServiceCollection services, IConfiguration configuration) + { + services.TryAddTransient(); + if (configuration.GetSection("AppSettings").Get()?.RequiredValidation == true) + { + services.TryAddTransient(); + } + + if (configuration.GetSection("AppSettings").Get()?.ExpressionValidation == true) + { + services.TryAddTransient(); + } + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + } + /// /// Checks if a service is already added to the collection. /// @@ -221,10 +259,19 @@ private static void AddAppOptions(IServiceCollection services) private static void AddProcessServices(IServiceCollection services) { services.TryAddTransient(); - services.TryAddTransient(); + services.TryAddTransient(); services.TryAddSingleton(); + services.TryAddTransient(); + services.AddTransient(); services.TryAddTransient(); - services.TryAddTransient(); + } + + private static void AddActionServices(IServiceCollection services) + { + services.TryAddTransient(); + services.AddTransient(); + services.AddHttpClient(); + services.AddTransientUserActionAuthorizerForActionInAllTasks("sign"); } private static void AddFileAnalyserServices(IServiceCollection services) @@ -238,5 +285,15 @@ private static void AddFileValidatorServices(IServiceCollection services) services.TryAddTransient(); services.TryAddTransient(); } + + private static void AddMetricsDecorators(IServiceCollection services, IConfiguration configuration) + { + MetricsSettings metricsSettings = configuration.GetSection("MetricsSettings")?.Get() ?? new MetricsSettings(); + if (metricsSettings.Enabled) + { + services.Decorate(); + services.Decorate(); + } + } } } diff --git a/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs b/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs index 9cf73be23..4f4d56084 100644 --- a/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs +++ b/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs @@ -27,7 +27,7 @@ public static class XmlToLinqExtensions /// public static XElement AddAttribute(this XElement element, string attributeName, object value) { - element.Add(new XAttribute(attributeName, value.ToString())); + element.Add(new XAttribute(attributeName, value.ToString()!)); return element; } diff --git a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs new file mode 100644 index 000000000..6ee1a37be --- /dev/null +++ b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs @@ -0,0 +1,90 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Sign; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.UserAction; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; +using Signee = Altinn.App.Core.Internal.Sign.Signee; + +namespace Altinn.App.Core.Features.Action; + +/// +/// Class handling tasks that should happen when action signing is performed. +/// +public class SigningUserAction: IUserAction +{ + private readonly IProcessReader _processReader; + private readonly ILogger _logger; + private readonly IProfileClient _profileClient; + private readonly ISignClient _signClient; + + /// + /// Initializes a new instance of the class + /// + /// The process reader + /// The logger + /// The profile client + /// The sign client + public SigningUserAction(IProcessReader processReader, ILogger logger, IProfileClient profileClient, ISignClient signClient) + { + _logger = logger; + _profileClient = profileClient; + _signClient = signClient; + _processReader = processReader; + } + + /// + public string Id => "sign"; + + /// + /// + /// + public async Task HandleAction(UserActionContext context) + { + if (_processReader.GetFlowElement(context.Instance.Process.CurrentTask.ElementId) is ProcessTask currentTask) + { + _logger.LogInformation("Signing action handler invoked for instance {Id}. In task: {CurrentTaskId}", context.Instance.Id, currentTask.Id); + var dataTypes = currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.DataTypesToSign ?? new(); + var connectedDataElements = GetDataElementSignatures(context.Instance.Data, dataTypes); + if (connectedDataElements.Count > 0 && currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.SignatureDataType != null) + { + SignatureContext signatureContext = new SignatureContext(new InstanceIdentifier(context.Instance), currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.SignatureDataType!, await GetSignee(context.UserId), connectedDataElements); + await _signClient.SignDataElements(signatureContext); + return UserActionResult.SuccessResult(); + } + + throw new ApplicationConfigException("Missing configuration for signing. Check that the task has a signature configuration and that the data types to sign are defined."); + } + + return UserActionResult.FailureResult(new ActionError() + { + Code = "NoProcessTask", + Message = "Current task is not a process task." + }); + } + + private static List GetDataElementSignatures(List dataElements, List dataTypesToSign) + { + var connectedDataElements = new List(); + foreach (var dataType in dataTypesToSign) + { + connectedDataElements.AddRange(dataElements.Where(d => d.DataType.Equals(dataType, StringComparison.OrdinalIgnoreCase)).Select(d => new DataElementSignature(d.Id))); + } + + return connectedDataElements; + } + + private async Task GetSignee(int userId) + { + var userProfile = await _profileClient.GetUserProfile(userId); + return new Signee + { + UserId = userProfile.UserId.ToString(), + PersonNumber = userProfile.Party.SSN, + OrganisationNumber = userProfile.Party.OrgNumber, + }; + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs b/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs new file mode 100644 index 000000000..ff9aef12d --- /dev/null +++ b/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs @@ -0,0 +1,83 @@ +using System.Text.Json; +using Altinn.App.Core.Extensions; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Action; + +/// +/// Implementation of IUserActionAuthorizer that checks if a signature is unique from other signatures defined in the list of dataTypes under uniqueFromSignaturesInDataTypes inside the bpmn file +/// +public class UniqueSignatureAuthorizer : IUserActionAuthorizer +{ + private readonly IAppMetadata _appMetadata; + private readonly IProcessReader _processReader; + private readonly IInstanceClient _instanceClient; + private readonly IDataClient _dataClient; + + /// + /// Intializes a new instance of the class + /// + /// The process reader + /// The instance client + /// The data client + /// The application metadata + public UniqueSignatureAuthorizer(IProcessReader processReader, IInstanceClient instanceClient, IDataClient dataClient, IAppMetadata appMetadata) + { + _processReader = processReader; + _instanceClient = instanceClient; + _dataClient = dataClient; + _appMetadata = appMetadata; + } + + /// + public async Task AuthorizeAction(UserActionAuthorizerContext context) + { + if (context.TaskId == null) + { + return true; + } + var flowElement = _processReader.GetFlowElement(context.TaskId) as ProcessTask; + if (flowElement?.ExtensionElements?.TaskExtension?.SignatureConfiguration?.UniqueFromSignaturesInDataTypes.Count > 0) + { + var appMetadata = await _appMetadata.GetApplicationMetadata(); + var instance = await _instanceClient.GetInstance(appMetadata.AppIdentifier.App, appMetadata.AppIdentifier.Org, context.InstanceIdentifier.InstanceOwnerPartyId, context.InstanceIdentifier.InstanceGuid); + var dataTypes = flowElement.ExtensionElements!.TaskExtension!.SignatureConfiguration!.UniqueFromSignaturesInDataTypes; + var signatureDataElements = instance.Data.Where(d => dataTypes.Contains(d.DataType)).ToList(); + foreach (var signatureDataElement in signatureDataElements) + { + var userId = await GetUserIdFromDataElementContainingSignDocument(appMetadata.AppIdentifier, context.InstanceIdentifier, signatureDataElement); + if (userId == context.User.GetUserOrOrgId()) + { + return false; + } + } + } + + return true; + } + + private async Task GetUserIdFromDataElementContainingSignDocument(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, DataElement dataElement) + { + await using var data = await _dataClient.GetBinaryData(appIdentifier.Org, appIdentifier.App, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, Guid.Parse(dataElement.Id)); + try + { + JsonSerializerOptions options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + }; + var signDocument = await JsonSerializer.DeserializeAsync(data, options); + return signDocument?.SigneeInfo.UserId ?? ""; + } + catch (JsonException) + { + return ""; + } + } +} diff --git a/src/Altinn.App.Core/Features/Action/UserActionAuthorizerContext.cs b/src/Altinn.App.Core/Features/Action/UserActionAuthorizerContext.cs new file mode 100644 index 000000000..145240843 --- /dev/null +++ b/src/Altinn.App.Core/Features/Action/UserActionAuthorizerContext.cs @@ -0,0 +1,45 @@ +using System.Security.Claims; +using Altinn.App.Core.Models; + +namespace Altinn.App.Core.Features.Action; + +/// +/// Context for authorization of user actions +/// +public class UserActionAuthorizerContext +{ + /// + /// Initializes a new instance of the class + /// + /// for the user + /// for the instance + /// The id of the task + /// The action to authorize + public UserActionAuthorizerContext(ClaimsPrincipal user, InstanceIdentifier instanceIdentifier, string? taskId, string action) + { + User = user; + InstanceIdentifier = instanceIdentifier; + TaskId = taskId; + Action = action; + } + + /// + /// Gets or sets the user + /// + public ClaimsPrincipal User { get; set; } + + /// + /// Gets or sets the instance identifier + /// + public InstanceIdentifier InstanceIdentifier { get; set; } + + /// + /// Gets or sets the task id + /// + public string? TaskId { get; set; } + + /// + /// Gets or sets the action + /// + public string Action { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Action/UserActionService.cs b/src/Altinn.App.Core/Features/Action/UserActionService.cs new file mode 100644 index 000000000..1e824916a --- /dev/null +++ b/src/Altinn.App.Core/Features/Action/UserActionService.cs @@ -0,0 +1,36 @@ +using Altinn.App.Core.Internal; + +namespace Altinn.App.Core.Features.Action; + +/// +/// Factory class for resolving implementations +/// based on the id of the action. +/// +public class UserActionService +{ + private readonly IEnumerable _actionHandlers; + + /// + /// Initializes a new instance of the class. + /// + /// The list of action handlers to choose from. + public UserActionService(IEnumerable actionHandlers) + { + _actionHandlers = actionHandlers; + } + + /// + /// Find the implementation of based on the actionId + /// + /// The id of the action to handle. + /// The first implementation of that matches the actionId. If no match null is returned + public IUserAction? GetActionHandler(string? actionId) + { + if (actionId != null) + { + return _actionHandlers.FirstOrDefault(ah => ah.Id.Equals(actionId, StringComparison.OrdinalIgnoreCase)); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/DataProcessing/GenericDataProcessor.cs b/src/Altinn.App.Core/Features/DataProcessing/GenericDataProcessor.cs new file mode 100644 index 000000000..6cfeb8171 --- /dev/null +++ b/src/Altinn.App.Core/Features/DataProcessing/GenericDataProcessor.cs @@ -0,0 +1,39 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.DataProcessing; + +/// +/// Convenience class for implementing for a specific model type. +/// +public abstract class GenericDataProcessor : IDataProcessor where TModel : class +{ + /// + /// Do changes to the model after it has been read from storage, but before it is returned to the app. + /// this only executes on page load and not for subsequent updates. + /// + public abstract Task ProcessDataRead(Instance instance, Guid? dataId, TModel model); + + /// + /// Do changes to the model before it is written to storage, and report back to frontend. + /// Tyipically used to add calculated values to the model. + /// + public abstract Task ProcessDataWrite(Instance instance, Guid? dataId, TModel model, TModel? previousModel); + + /// + public async Task ProcessDataRead(Instance instance, Guid? dataId, object data) + { + if (data is TModel model) + { + await ProcessDataRead(instance, dataId, model); + } + } + + /// + public async Task ProcessDataWrite(Instance instance, Guid? dataId, object data, object? previousData) + { + if (data is TModel model) + { + await ProcessDataWrite(instance, dataId, model, previousData as TModel); + } + } +} diff --git a/src/Altinn.App.Core/Features/DataProcessing/NullDataProcessor.cs b/src/Altinn.App.Core/Features/DataProcessing/NullDataProcessor.cs deleted file mode 100644 index aa2b0c6df..000000000 --- a/src/Altinn.App.Core/Features/DataProcessing/NullDataProcessor.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Features.DataProcessing; - -/// -/// Default implementation of the IDataProcessor interface. -/// This implementation does not do any thing to the data -/// -public class NullDataProcessor: IDataProcessor -{ - /// - public async Task ProcessDataRead(Instance instance, Guid? dataId, object data) - { - return await Task.FromResult(false); - } - - /// - public async Task ProcessDataWrite(Instance instance, Guid? dataId, object data) - { - return await Task.FromResult(false); - } -} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs index 6bbf62d56..279cd97d3 100644 --- a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs +++ b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs @@ -37,7 +37,7 @@ public FileAnalysisResult(string analyserId) public string? MimeType { get; set; } /// - /// Key/Value pairs contaning fining from the analysis. + /// Key/Value pairs containing findings from the analysis. /// public IDictionary Metadata { get; private set; } = new Dictionary(); } diff --git a/src/Altinn.App.Core/Features/IDataElementValidator.cs b/src/Altinn.App.Core/Features/IDataElementValidator.cs new file mode 100644 index 000000000..b05b4f8eb --- /dev/null +++ b/src/Altinn.App.Core/Features/IDataElementValidator.cs @@ -0,0 +1,37 @@ +using Altinn.App.Core.Features.FileAnalysis; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Validation; + +/// +/// Validator for data elements. +/// See for an alternative validator for data elements with app logic. +/// and that support incremental validation on save. +/// For validating the content of files, see and +/// +public interface IDataElementValidator +{ + /// + /// The data type that this validator should run for. This is the id of the data type from applicationmetadata.json + /// + /// + /// + /// + string DataType { get; } + + /// + /// Returns the group id of the validator. + /// The default is based on the FullName and DataType fields, and should not need customization + /// + string ValidationSource => $"{this.GetType().FullName}-{DataType}"; + + /// + /// Run validations for a data element. This is supposed to run quickly + /// + /// The instance to validate + /// + /// + /// + public Task> ValidateDataElement(Instance instance, DataElement dataElement, DataType dataType); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/IDataProcessor.cs b/src/Altinn.App.Core/Features/IDataProcessor.cs index fb8fcc9d4..0c1825376 100644 --- a/src/Altinn.App.Core/Features/IDataProcessor.cs +++ b/src/Altinn.App.Core/Features/IDataProcessor.cs @@ -11,15 +11,16 @@ public interface IDataProcessor /// Is called to run custom calculation events defined by app developer when data is read from app /// /// Instance that data belongs to - /// Data id for the data + /// Data id for the data (nullable if stateless) /// The data to perform calculations on - public Task ProcessDataRead(Instance instance, Guid? dataId, object data); - + public Task ProcessDataRead(Instance instance, Guid? dataId, object data); + /// /// Is called to run custom calculation events defined by app developer when data is written to app /// /// Instance that data belongs to - /// Data id for the data + /// Data id for the data (nullable if stateless) /// The data to perform calculations on - public Task ProcessDataWrite(Instance instance, Guid? dataId, object data); + /// The previous data model (for running comparisons) + public Task ProcessDataWrite(Instance instance, Guid? dataId, object data, object? previousData); } \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/IFormDataValidator.cs b/src/Altinn.App.Core/Features/IFormDataValidator.cs new file mode 100644 index 000000000..d6c4da3f6 --- /dev/null +++ b/src/Altinn.App.Core/Features/IFormDataValidator.cs @@ -0,0 +1,42 @@ +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace Altinn.App.Core.Features; + +/// +/// Interface for handling validation of form data. +/// (i.e. dataElements with AppLogic defined +/// +public interface IFormDataValidator +{ + /// + /// The data type this validator is for. Typically either hard coded by implementation or + /// or set by constructor using a and a keyed service. + /// + /// To validate all types with form data, just use a "*" as value + /// + string DataType { get; } + + /// + /// Used for partial validation to ensure that the validator only runs when relevant fields have changed. + /// + /// The current state of the form data + /// The previous state of the form data + bool HasRelevantChanges(object current, object previous); + + /// + /// Returns the group id of the validator. This is used to run partial validations on the backend. + /// The default is based on the FullName and DataType fields, and should not need customization + /// + public string ValidationSource => $"{this.GetType().FullName}-{DataType}"; + + /// + /// The actual validation function + /// + /// + /// + /// + /// List of validation issues + Task> ValidateFormData(Instance instance, DataElement dataElement, object data); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/IInstanceValidator.cs b/src/Altinn.App.Core/Features/IInstanceValidator.cs index 929e65e49..956d0aaeb 100644 --- a/src/Altinn.App.Core/Features/IInstanceValidator.cs +++ b/src/Altinn.App.Core/Features/IInstanceValidator.cs @@ -1,3 +1,4 @@ +using Altinn.App.Core.Features.Validation; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -6,6 +7,7 @@ namespace Altinn.App.Core.Features; /// /// IInstanceValidator defines the methods that are used to validate data and tasks /// +[Obsolete($"Use {nameof(ITaskValidator)}, {nameof(IDataElementValidator)} or {nameof(IFormDataValidator)} instead")] public interface IInstanceValidator { /// diff --git a/src/Altinn.App.Core/Features/IPageOrder.cs b/src/Altinn.App.Core/Features/IPageOrder.cs index b69ef056b..43b94890a 100644 --- a/src/Altinn.App.Core/Features/IPageOrder.cs +++ b/src/Altinn.App.Core/Features/IPageOrder.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Features /// /// Interface for page order handling in stateful apps /// + [Obsolete("IPageOrder does not work with frontend version 4")] public interface IPageOrder { /// diff --git a/src/Altinn.App.Core/Features/IProcessExclusiveGateway.cs b/src/Altinn.App.Core/Features/IProcessExclusiveGateway.cs index 32c8d5095..047df0af7 100644 --- a/src/Altinn.App.Core/Features/IProcessExclusiveGateway.cs +++ b/src/Altinn.App.Core/Features/IProcessExclusiveGateway.cs @@ -1,4 +1,5 @@ using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Models.Process; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Features; @@ -9,7 +10,7 @@ namespace Altinn.App.Core.Features; public interface IProcessExclusiveGateway { /// - /// + /// Id of the gateway in the BPMN process this filter applies to /// string GatewayId { get; } @@ -18,6 +19,7 @@ public interface IProcessExclusiveGateway /// /// Complete list of defined flows out of gateway /// Instance where process is about to move next - /// - public Task> FilterAsync(List outgoingFlows, Instance instance); + /// Information connected with the current gateway under evaluation + /// List of possible SequenceFlows to choose out of the gateway + public Task> FilterAsync(List outgoingFlows, Instance instance, ProcessGatewayInformation processGatewayInformation); } diff --git a/src/Altinn.App.Core/Features/ITaskValidator.cs b/src/Altinn.App.Core/Features/ITaskValidator.cs new file mode 100644 index 000000000..d63479c14 --- /dev/null +++ b/src/Altinn.App.Core/Features/ITaskValidator.cs @@ -0,0 +1,41 @@ +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace Altinn.App.Core.Features; + +/// +/// Interface for handling validation of tasks. +/// +public interface ITaskValidator +{ + /// + /// The task id this validator is for. Typically either hard coded by implementation or + /// or set by constructor using a and a keyed service. + /// + /// + /// + /// string TaskId { get; init; } + /// // constructor + /// public MyTaskValidator([ServiceKey] string taskId) + /// { + /// TaskId = taskId; + /// } + /// + /// + string TaskId { get; } + + /// + /// Returns the group id of the validator. + /// The default is based on the FullName and TaskId fields, and should not need customization + /// + string ValidationSource => $"{this.GetType().FullName}-{TaskId}"; + + /// + /// Actual validation logic for the task + /// + /// The instance to validate + /// current task to run validations for + /// List of validation issues to add to this task validation + Task> ValidateTask(Instance instance, string taskId); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/IUserAction.cs b/src/Altinn.App.Core/Features/IUserAction.cs new file mode 100644 index 000000000..9b39a3368 --- /dev/null +++ b/src/Altinn.App.Core/Features/IUserAction.cs @@ -0,0 +1,21 @@ +using Altinn.App.Core.Models.UserAction; + +namespace Altinn.App.Core.Features; + +/// +/// Interface for implementing custom code for user actions +/// +public interface IUserAction +{ + /// + /// The id of the user action + /// + string Id { get; } + + /// + /// Method for handling the user action + /// + /// The user action context + /// If the handling of the action was a success + Task HandleAction(UserActionContext context); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/IUserActionAuthorizer.cs b/src/Altinn.App.Core/Features/IUserActionAuthorizer.cs new file mode 100644 index 000000000..ec1c6ab5e --- /dev/null +++ b/src/Altinn.App.Core/Features/IUserActionAuthorizer.cs @@ -0,0 +1,16 @@ +using Altinn.App.Core.Features.Action; + +namespace Altinn.App.Core.Features; + +/// +/// Interface for writing custom authorization logic for actions in the app that cannot be handled by the default authorization policies +/// +public interface IUserActionAuthorizer +{ + /// + /// Authorizes the action in the given context + /// + /// for the action to authorize + /// true if user is authorized to perform the action, false if not + Task AuthorizeAction(UserActionAuthorizerContext context); +} diff --git a/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2MetadataApiClient.cs b/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2MetadataApiClient.cs index dfb9fafde..4d117789d 100644 --- a/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2MetadataApiClient.cs +++ b/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2MetadataApiClient.cs @@ -1,4 +1,6 @@ #nullable enable +using Altinn.App.Core.Helpers; + namespace Altinn.App.Core.Features.Options.Altinn2Provider { /// @@ -33,7 +35,7 @@ public async Task GetAltinn2Codelist(string id, string response = await _client.GetAsync($"https://www.altinn.no/api/metadata/codelists/{id}/{version?.ToString() ?? string.Empty}"); } response.EnsureSuccessStatusCode(); - var codelist = await response.Content.ReadAsAsync(); + var codelist = await JsonSerializerPermissive.DeserializeAsync(response.Content); return codelist; } } diff --git a/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs b/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs index 43984971b..e38804728 100644 --- a/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs +++ b/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; namespace Altinn.App.Core.Features.PageOrder @@ -6,6 +6,7 @@ namespace Altinn.App.Core.Features.PageOrder /// /// Interface for page order handling in stateless apps /// + [Obsolete("IPageOrder does not work with frontend version 4")] public class DefaultPageOrder : IPageOrder { private readonly IAppResources _resources; diff --git a/src/Altinn.App.Core/Features/Validation/Default/DataAnnotationValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/DataAnnotationValidator.cs new file mode 100644 index 000000000..1ec517df2 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Default/DataAnnotationValidator.cs @@ -0,0 +1,69 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Validation.Helpers; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Core.Features.Validation.Default; + +/// +/// Runs validation on the data object. +/// +public class DataAnnotationValidator : IFormDataValidator +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IObjectModelValidator _objectModelValidator; + private readonly GeneralSettings _generalSettings; + + /// + /// Constructor + /// + public DataAnnotationValidator(IHttpContextAccessor httpContextAccessor, IObjectModelValidator objectModelValidator, IOptions generalSettings) + { + _httpContextAccessor = httpContextAccessor; + _objectModelValidator = objectModelValidator; + _generalSettings = generalSettings.Value; + } + + /// + /// Run Data annotation validation on all data types with app logic + /// + public string DataType => "*"; + + /// + /// This validator has the code "DataAnnotations" and this is known by the frontend, who may request this validator to not run for incremental validation. + /// + public string ValidationSource => "DataAnnotations"; + + /// + /// We don't know which fields are relevant for data annotation validation, so we always run it. + /// + public bool HasRelevantChanges(object current, object previous) => true; + + /// + public Task> ValidateFormData(Instance instance, DataElement dataElement, object data) + { + try + { + var modelState = new ModelStateDictionary(); + var actionContext = new ActionContext( + _httpContextAccessor.HttpContext!, + new Microsoft.AspNetCore.Routing.RouteData(), + new ActionDescriptor(), + modelState); + ValidationStateDictionary validationState = new ValidationStateDictionary(); + _objectModelValidator.Validate(actionContext, validationState, null!, data); + + return Task.FromResult(ModelStateHelpers.ModelStateToIssueList(modelState, instance, dataElement, _generalSettings, data.GetType(), ValidationIssueSources.ModelState)); + } + catch (Exception e) + { + return Task.FromException>(e); + } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/Default/DefaultDataElementValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/DefaultDataElementValidator.cs new file mode 100644 index 000000000..28a94a041 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Default/DefaultDataElementValidator.cs @@ -0,0 +1,90 @@ +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Enums; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Validation.Default; + +/// +/// Default validations that run on all data elements to validate metadata and file scan results. +/// +public class DefaultDataElementValidator : IDataElementValidator +{ + /// + /// Run validations on all data elements + /// + public string DataType => "*"; + + /// + public Task> ValidateDataElement(Instance instance, DataElement dataElement, DataType dataType) + { + var issues = new List(); + if (dataElement.ContentType == null) + { + issues.Add( new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.MissingContentType, + DataElementId = dataElement.Id, + Severity = ValidationIssueSeverity.Error, + Description = ValidationIssueCodes.DataElementCodes.MissingContentType + }); + } + else + { + var contentTypeWithoutEncoding = dataElement.ContentType.Split(";")[0]; + + if (dataType.AllowedContentTypes != null && dataType.AllowedContentTypes.Count > 0 && + dataType.AllowedContentTypes.TrueForAll(ct => + !ct.Equals(contentTypeWithoutEncoding, StringComparison.OrdinalIgnoreCase))) + { + issues.Add( new ValidationIssue + { + DataElementId = dataElement.Id, + Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, + Severity = ValidationIssueSeverity.Error, + Description = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, + Field = dataType.Id + }); + } + } + + if (dataType.MaxSize.HasValue && dataType.MaxSize > 0 && + (long)dataType.MaxSize * 1024 * 1024 < dataElement.Size) + { + issues.Add( new ValidationIssue + { + DataElementId = dataElement.Id, + Code = ValidationIssueCodes.DataElementCodes.DataElementTooLarge, + Severity = ValidationIssueSeverity.Error, + Description = ValidationIssueCodes.DataElementCodes.DataElementTooLarge, + Field = dataType.Id + }); + } + + if (dataType.EnableFileScan && dataElement.FileScanResult == FileScanResult.Infected) + { + issues.Add( new ValidationIssue + { + DataElementId = dataElement.Id, + Code = ValidationIssueCodes.DataElementCodes.DataElementFileInfected, + Severity = ValidationIssueSeverity.Error, + Description = ValidationIssueCodes.DataElementCodes.DataElementFileInfected, + Field = dataType.Id + }); + } + + if (dataType.EnableFileScan && dataType.ValidationErrorOnPendingFileScan && + dataElement.FileScanResult == FileScanResult.Pending) + { + issues.Add( new ValidationIssue + { + DataElementId = dataElement.Id, + Code = ValidationIssueCodes.DataElementCodes.DataElementFileScanPending, + Severity = ValidationIssueSeverity.Error, + Description = ValidationIssueCodes.DataElementCodes.DataElementFileScanPending, + Field = dataType.Id + }); + } + + return Task.FromResult(issues); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/Default/DefaultTaskValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/DefaultTaskValidator.cs new file mode 100644 index 000000000..a01b98bd6 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Default/DefaultTaskValidator.cs @@ -0,0 +1,64 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Enums; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace Altinn.App.Core.Features.Validation.Default; + +/// +/// Implement the default validation of DataElements based on the metadata in appMetadata +/// +public class DefaultTaskValidator : ITaskValidator +{ + private readonly IAppMetadata _appMetadata; + + /// + /// Initializes a new instance of the class. + /// + public DefaultTaskValidator(IAppMetadata appMetadata) + { + _appMetadata = appMetadata; + } + + /// + public string TaskId => "*"; + + /// + public async Task> ValidateTask(Instance instance, string taskId) + { + var messages = new List(); + var application = await _appMetadata.GetApplicationMetadata(); + + foreach (var dataType in application.DataTypes.Where(et => et.TaskId == taskId)) + { + List elements = instance.Data.Where(d => d.DataType == dataType.Id).ToList(); + + if (dataType.MaxCount > 0 && dataType.MaxCount < elements.Count) + { + var message = new ValidationIssue + { + Code = ValidationIssueCodes.InstanceCodes.TooManyDataElementsOfType, + Severity = ValidationIssueSeverity.Error, + Description = ValidationIssueCodes.InstanceCodes.TooManyDataElementsOfType, + Field = dataType.Id + }; + messages.Add(message); + } + + if (dataType.MinCount > 0 && dataType.MinCount > elements.Count) + { + var message = new ValidationIssue + { + Code = ValidationIssueCodes.InstanceCodes.TooFewDataElementsOfType, + Severity = ValidationIssueSeverity.Error, + Description = ValidationIssueCodes.InstanceCodes.TooFewDataElementsOfType, + Field = dataType.Id + }; + messages.Add(message); + } + } + + return messages; + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/Default/ExpressionValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/ExpressionValidator.cs new file mode 100644 index 000000000..df60177c8 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Default/ExpressionValidator.cs @@ -0,0 +1,303 @@ +using System.Text.Json; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; + +namespace Altinn.App.Core.Features.Validation.Default; + +/// +/// Validates form data against expression validations +/// +public class ExpressionValidator : IFormDataValidator +{ + private readonly ILogger _logger; + private readonly IAppResources _appResourceService; + private readonly LayoutEvaluatorStateInitializer _layoutEvaluatorStateInitializer; + + private static readonly JsonSerializerOptions _jsonOptions = new() + { + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + /// + /// Constructor for the expression validator + /// + public ExpressionValidator(ILogger logger, IAppResources appResourceService, LayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer) + { + _logger = logger; + _appResourceService = appResourceService; + _layoutEvaluatorStateInitializer = layoutEvaluatorStateInitializer; + } + + /// + public string DataType => "*"; + + /// + /// This validator has the code "Expression" and this is known by the frontend, who may request this validator to not run for incremental validation. + /// + public string ValidationSource => "Expression"; + + /// + /// Expression validations should always run (it is way to complex to figure out if it should run or not) + /// + public bool HasRelevantChanges(object current, object previous) => true; + + /// + public async Task> ValidateFormData(Instance instance, DataElement dataElement, object data) + { + var rawValidationConfig = _appResourceService.GetValidationConfiguration(dataElement.DataType); + if (rawValidationConfig == null) + { + // No validation configuration exists for this data type + return new List(); + } + + var validationConfig = JsonDocument.Parse(rawValidationConfig).RootElement; + var evaluatorState = await _layoutEvaluatorStateInitializer.Init(instance, data, dataElement.Id); + return Validate(validationConfig, evaluatorState, _logger); + } + + + internal static List Validate(JsonElement validationConfig, LayoutEvaluatorState evaluatorState, ILogger logger) + { + var validationIssues = new List(); + var expressionValidations = ParseExpressionValidationConfig(validationConfig, logger); + foreach (var validationObject in expressionValidations) + { + var baseField = validationObject.Key; + var resolvedFields = evaluatorState.GetResolvedKeys(baseField); + var validations = validationObject.Value; + foreach (var resolvedField in resolvedFields) + { + var positionalArguments = new[] { resolvedField }; + foreach (var validation in validations) + { + try + { + if (validation.Condition == null) + { + continue; + } + + var isInvalid = ExpressionEvaluator.EvaluateExpression(evaluatorState, validation.Condition, null, positionalArguments); + if (isInvalid is not bool) + { + throw new ArgumentException($"Validation condition for {resolvedField} did not evaluate to a boolean"); + } + if ((bool)isInvalid) + { + var validationIssue = new ValidationIssue + { + Field = resolvedField, + Severity = validation.Severity ?? ValidationIssueSeverity.Error, + CustomTextKey = validation.Message, + Code = validation.Message, + Source = ValidationIssueSources.Expression, + }; + validationIssues.Add(validationIssue); + } + } + catch(Exception e) + { + logger.LogError(e, "Error while evaluating expression validation for {resolvedField}", resolvedField); + throw; + } + } + } + } + + + return validationIssues; + } + + private static RawExpressionValidation? ResolveValidationDefinition(string name, JsonElement definition, Dictionary resolvedDefinitions, ILogger logger) + { + var resolvedDefinition = new RawExpressionValidation(); + + var rawDefinition = definition.Deserialize(_jsonOptions); + if (rawDefinition == null) + { + logger.LogError("Validation definition {name} could not be parsed", name); + return null; + } + if (rawDefinition.Ref != null) + { + var reference = resolvedDefinitions.GetValueOrDefault(rawDefinition.Ref); + if (reference == null) + { + logger.LogError("Could not resolve reference {rawDefinitionRef} for validation {name}", rawDefinition.Ref, name); + return null; + + } + resolvedDefinition.Message = reference.Message; + resolvedDefinition.Condition = reference.Condition; + resolvedDefinition.Severity = reference.Severity; + } + + if (rawDefinition.Message != null) + { + resolvedDefinition.Message = rawDefinition.Message; + } + + if (rawDefinition.Condition != null) + { + resolvedDefinition.Condition = rawDefinition.Condition; + } + + if (rawDefinition.Severity != null) + { + resolvedDefinition.Severity = rawDefinition.Severity; + } + + if (resolvedDefinition.Message == null) + { + logger.LogError("Validation {name} is missing message", name); + return null; + } + + if (resolvedDefinition.Condition == null) + { + logger.LogError("Validation {name} is missing condition", name); + return null; + } + + return resolvedDefinition; + } + + private static ExpressionValidation? ResolveExpressionValidation(string field, JsonElement definition, Dictionary resolvedDefinitions, ILogger logger) + { + + var rawExpressionValidatıon = new RawExpressionValidation(); + + if (definition.ValueKind == JsonValueKind.String) + { + var stringReference = definition.GetString(); + if (stringReference == null) + { + logger.LogError("Could not resolve null reference for validation for field {field}", field); + return null; + } + var reference = resolvedDefinitions.GetValueOrDefault(stringReference); + if (reference == null) + { + logger.LogError("Could not resolve reference {stringReference} for validation for field {field}", stringReference, field); + return null; + } + rawExpressionValidatıon.Message = reference.Message; + rawExpressionValidatıon.Condition = reference.Condition; + rawExpressionValidatıon.Severity = reference.Severity; + } + else + { + var expressionDefinition = definition.Deserialize(_jsonOptions); + if (expressionDefinition == null) + { + logger.LogError("Validation for field {field} could not be parsed", field); + return null; + } + + if (expressionDefinition.Ref != null) + { + var reference = resolvedDefinitions.GetValueOrDefault(expressionDefinition.Ref); + if (reference == null) + { + logger.LogError("Could not resolve reference {expressionDefinitionRef} for validation for field {field}", expressionDefinition.Ref, field); + return null; + + } + rawExpressionValidatıon.Message = reference.Message; + rawExpressionValidatıon.Condition = reference.Condition; + rawExpressionValidatıon.Severity = reference.Severity; + } + + if (expressionDefinition.Message != null) + { + rawExpressionValidatıon.Message = expressionDefinition.Message; + } + + if (expressionDefinition.Condition != null) + { + rawExpressionValidatıon.Condition = expressionDefinition.Condition; + } + + if (expressionDefinition.Severity != null) + { + rawExpressionValidatıon.Severity = expressionDefinition.Severity; + } + } + + if (rawExpressionValidatıon.Message == null) + { + logger.LogError("Validation for field {field} is missing message", field); + return null; + } + + if (rawExpressionValidatıon.Condition == null) + { + logger.LogError("Validation for field {field} is missing condition", field); + return null; + } + + var expressionValidation = new ExpressionValidation + { + Message = rawExpressionValidatıon.Message, + Condition = rawExpressionValidatıon.Condition, + Severity = rawExpressionValidatıon.Severity ?? ValidationIssueSeverity.Error, + }; + + return expressionValidation; + } + + private static Dictionary> ParseExpressionValidationConfig(JsonElement expressionValidationConfig, ILogger logger) + { + var expressionValidationDefinitions = new Dictionary(); + JsonElement definitionsObject; + var hasDefinitions = expressionValidationConfig.TryGetProperty("definitions", out definitionsObject); + if (hasDefinitions) + { + foreach (var definitionObject in definitionsObject.EnumerateObject()) + { + var name = definitionObject.Name; + var definition = definitionObject.Value; + var resolvedDefinition = ResolveValidationDefinition(name, definition, expressionValidationDefinitions, logger); + if (resolvedDefinition == null) + { + logger.LogError("Validation definition {name} could not be resolved", name); + continue; + } + expressionValidationDefinitions[name] = resolvedDefinition; + } + } + var expressionValidations = new Dictionary>(); + JsonElement validationsObject; + var hasValidations = expressionValidationConfig.TryGetProperty("validations", out validationsObject); + if (hasValidations) + { + foreach (var validationArray in validationsObject.EnumerateObject()) + { + var field = validationArray.Name; + var validations = validationArray.Value; + foreach (var validation in validations.EnumerateArray()) + { + if (!expressionValidations.TryGetValue(field, out var expressionValidation)) + { + expressionValidation = new List(); + expressionValidations[field] = expressionValidation; + } + var resolvedExpressionValidation = ResolveExpressionValidation(field, validation, expressionValidationDefinitions, logger); + if (resolvedExpressionValidation == null) + { + logger.LogError("Validation for field {field} could not be resolved", field); + continue; + } + expressionValidation.Add(resolvedExpressionValidation); + } + } + } + return expressionValidations; + } + +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorFormDataValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorFormDataValidator.cs new file mode 100644 index 000000000..51de77bf2 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorFormDataValidator.cs @@ -0,0 +1,51 @@ +#pragma warning disable CS0618 // Type or member is obsolete +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Validation.Helpers; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Core.Features.Validation.Default; + +/// +/// This validator is used to run the legacy IInstanceValidator.ValidateData method +/// +public class LegacyIInstanceValidatorFormDataValidator : IFormDataValidator +{ + private readonly IInstanceValidator? _instanceValidator; + private readonly GeneralSettings _generalSettings; + + /// + /// constructor + /// + public LegacyIInstanceValidatorFormDataValidator(IInstanceValidator? instanceValidator, IOptions generalSettings) + { + _instanceValidator = instanceValidator; + _generalSettings = generalSettings.Value; + } + + /// + /// The legacy validator should run for all data types + /// + public string DataType => "*"; + + /// + /// Always run for incremental validation (if it exists) + /// + public bool HasRelevantChanges(object current, object previous) => _instanceValidator is not null; + + + /// + public async Task> ValidateFormData(Instance instance, DataElement dataElement, object data) + { + if (_instanceValidator is null) + { + return new List(); + } + + var modelState = new ModelStateDictionary(); + await _instanceValidator.ValidateData(data, modelState); + return ModelStateHelpers.ModelStateToIssueList(modelState, instance, dataElement, _generalSettings, data.GetType(), ValidationIssueSources.Custom); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorTaskValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorTaskValidator.cs new file mode 100644 index 000000000..9f44fb4c9 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorTaskValidator.cs @@ -0,0 +1,49 @@ +#pragma warning disable CS0618 // Type or member is obsolete +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Validation.Helpers; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Core.Features.Validation.Default; + +/// +/// Ensures that the old extention hook is still supported. +/// +public class LegacyIInstanceValidatorTaskValidator : ITaskValidator +{ + private readonly IInstanceValidator? _instanceValidator; + private readonly GeneralSettings _generalSettings; + + /// + /// Constructor + /// + public LegacyIInstanceValidatorTaskValidator(IOptions generalSettings, IInstanceValidator? instanceValidator = null) + { + _instanceValidator = instanceValidator; + _generalSettings = generalSettings.Value; + } + + /// + /// Run the legacy validator for all tasks + /// + public string TaskId => "*"; + + /// + public string ValidationSource => _instanceValidator?.GetType().FullName ?? GetType().FullName!; + + /// + public async Task> ValidateTask(Instance instance, string taskId) + { + if (_instanceValidator is null) + { + return new List(); + } + + var modelState = new ModelStateDictionary(); + await _instanceValidator.ValidateTask(instance, taskId, modelState); + return ModelStateHelpers.MapModelStateToIssueList(modelState, instance, _generalSettings); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/Default/RequiredValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/RequiredValidator.cs new file mode 100644 index 000000000..ebb5e39dc --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Default/RequiredValidator.cs @@ -0,0 +1,52 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Validation.Default; + +/// +/// Validator that runs the required rules in the layout +/// +public class RequiredLayoutValidator : IFormDataValidator +{ + private readonly LayoutEvaluatorStateInitializer _layoutEvaluatorStateInitializer; + private readonly IAppResources _appResourcesService; + private readonly IAppMetadata _appMetadata; + + /// + /// Initializes a new instance of the class. + /// + public RequiredLayoutValidator(LayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, IAppResources appResourcesService, IAppMetadata appMetadata) + { + _layoutEvaluatorStateInitializer = layoutEvaluatorStateInitializer; + _appResourcesService = appResourcesService; + _appMetadata = appMetadata; + } + + /// + /// Run for all data types + /// + public string DataType => "*"; + + /// + /// This validator has the code "Required" and this is known by the frontend, who may request this validator to not run for incremental validation. + /// + public string ValidationSource => "Required"; + + /// + /// Always run for incremental validation + /// + public bool HasRelevantChanges(object current, object previous) => true; + + /// + /// Validate the form data against the required rules in the layout + /// + public async Task> ValidateFormData(Instance instance, DataElement dataElement, object data) + { + var appMetadata = await _appMetadata.GetApplicationMetadata(); + var layoutSet = _appResourcesService.GetLayoutSetForTask(appMetadata.DataTypes.First(dt=>dt.Id == dataElement.DataType).TaskId); + var evaluationState = await _layoutEvaluatorStateInitializer.Init(instance, data, layoutSet?.Id); + return LayoutEvaluator.RunLayoutValidationsForRequired(evaluationState, dataElement.Id); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/GenericFormDataValidator.cs b/src/Altinn.App.Core/Features/Validation/GenericFormDataValidator.cs new file mode 100644 index 000000000..f09d26585 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/GenericFormDataValidator.cs @@ -0,0 +1,109 @@ +using System.Diagnostics; +using System.Linq.Expressions; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Validation; + +/// +/// Simple wrapper for validation of form data that does the type checking for you. +/// +/// The type of the model this class will validate +public abstract class GenericFormDataValidator : IFormDataValidator +{ + /// + /// Constructor to force the DataType to be set. + /// + /// The data type this validator should run on + protected GenericFormDataValidator(string dataType) + { + DataType = dataType; + } + /// + public string DataType { get; private init; } + + // ReSharper disable once StaticMemberInGenericType + private static readonly AsyncLocal> ValidationIssues = new(); + + /// + /// Default implementation that respects the runFor prefixes. + /// + public bool HasRelevantChanges(object current, object previous) + { + if (current is not TModel currentCast) + { + throw new Exception($"{GetType().Name} wants to run on data type {DataType}, but the data is of type {current?.GetType().Name}. It should be of type {typeof(TModel).Name}"); + } + + if (previous is not TModel previousCast) + { + throw new Exception($"{GetType().Name} wants to run on data type {DataType}, but the previous of type {previous?.GetType().Name}. It should be of type {typeof(TModel).Name}"); + } + + return HasRelevantChanges(currentCast, previousCast); + } + + + /// + /// Convenience method to create a validation issue for a field using a linq expression instead of a json path for field + /// + /// An expression that is used to attach the issue to a path in the data model + /// The key used to lookup translations for the issue (displayed if lookup fails) + /// The severity for the issue (default Error) + /// Optional description if you want to provide a user friendly message that don't rely on the translation system + /// optional short code for the type of issue + /// List of parameters to replace after looking up the translation. Zero indexed {0} + protected void CreateValidationIssue(Expression> selector, string textKey, ValidationIssueSeverity severity = ValidationIssueSeverity.Error, string? description = null, string? code = null, List? customTextParams = null) + { + Debug.Assert(ValidationIssues.Value is not null); + AddValidationIssue(new ValidationIssue + { + Field = LinqExpressionHelpers.GetJsonPath(selector), + Description = description ?? textKey, + Code = code ?? textKey, + CustomTextKey = textKey, + CustomTextParams = customTextParams, + Severity = severity + }); + } + + /// + /// Allows inheriting classes to add validation issues. + /// + protected void AddValidationIssue(ValidationIssue issue) + { + Debug.Assert(ValidationIssues.Value is not null); + ValidationIssues.Value.Add(issue); + } + + /// + /// Implementation of the generic interface to call the correctly typed + /// validation method implemented by the inheriting class. + /// + public async Task> ValidateFormData(Instance instance, DataElement dataElement, object data) + { + if (data is not TModel model) + { + throw new ArgumentException($"Data is not of type {typeof(TModel)}"); + } + + ValidationIssues.Value = new List(); + await ValidateFormData(instance, dataElement, model); + return ValidationIssues.Value; + + } + + /// + /// Implement this method to validate the data. + /// + protected abstract Task ValidateFormData(Instance instance, DataElement dataElement, TModel data); + + /// + /// Implement this method to check if the data has changed in a way that requires validation. + /// + /// The current data model after applying patches and data processing + /// The previous state before patches and data processing + /// true if the list of validation issues might be different on the two model states + protected abstract bool HasRelevantChanges(TModel current, TModel previous); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/Helpers/ModelStateHelpers.cs b/src/Altinn.App.Core/Features/Validation/Helpers/ModelStateHelpers.cs new file mode 100644 index 000000000..5979663e9 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Helpers/ModelStateHelpers.cs @@ -0,0 +1,174 @@ +using System.Collections; +using System.Text.Json.Serialization; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Validation.Default; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Altinn.App.Core.Features.Validation.Helpers; + +/// +/// Static helpers to make map from to list of +/// +public static class ModelStateHelpers +{ + /// + /// Get a list of issues from a + /// + /// + /// The instance used for populating issue.InstanceId + /// Data element for populating issue.DataElementId + /// General settings to get *Fixed* prefixes + /// Type of the object to map ModelStateDictionary key to the json path field (might be different) + /// issue.Source + /// A list of the issues as our standard ValidationIssue + public static List ModelStateToIssueList(ModelStateDictionary modelState, Instance instance, + DataElement dataElement, GeneralSettings generalSettings, Type objectType, string source) + { + var validationIssues = new List(); + + foreach (var modelKey in modelState.Keys) + { + modelState.TryGetValue(modelKey, out var entry); + + if (entry is { ValidationState: ModelValidationState.Invalid }) + { + foreach (var error in entry.Errors) + { + var severityAndMessage = GetSeverityFromMessage(error.ErrorMessage, generalSettings); + validationIssues.Add(new ValidationIssue + { + DataElementId = dataElement.Id, + Source = source, + Code = severityAndMessage.Message, + Field = ModelKeyToField(modelKey, objectType)!, + Severity = severityAndMessage.Severity, + Description = severityAndMessage.Message + }); + } + } + } + + return validationIssues; + } + + private static (ValidationIssueSeverity Severity, string Message) GetSeverityFromMessage(string originalMessage, + GeneralSettings generalSettings) + { + if (originalMessage.StartsWith(generalSettings.SoftValidationPrefix)) + { + return (ValidationIssueSeverity.Warning, + originalMessage.Remove(0, generalSettings.SoftValidationPrefix.Length)); + } + +#pragma warning disable CS0618 // Type or member is obsolete + if (originalMessage.StartsWith(generalSettings.FixedValidationPrefix)) + { + return (ValidationIssueSeverity.Fixed, + originalMessage.Remove(0, generalSettings.FixedValidationPrefix.Length)); + } +#pragma warning restore CS0618 // Type or member is obsolete + + if (originalMessage.StartsWith(generalSettings.InfoValidationPrefix)) + { + return (ValidationIssueSeverity.Informational, + originalMessage.Remove(0, generalSettings.InfoValidationPrefix.Length)); + } + + if (originalMessage.StartsWith(generalSettings.SuccessValidationPrefix)) + { + return (ValidationIssueSeverity.Success, + originalMessage.Remove(0, generalSettings.SuccessValidationPrefix.Length)); + } + + return (ValidationIssueSeverity.Error, originalMessage); + } + + /// + /// Translate the ModelKey from validation to a field that respects [JsonPropertyName] annotations + /// + /// + /// Will be obsolete when updating to net70 or higher and activating + /// https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-7.0#use-json-property-names-in-validation-errors + /// + public static string? ModelKeyToField(string? modelKey, Type data) + { + var keyParts = modelKey?.Split('.', 2); + var keyWithIndex = keyParts?.ElementAtOrDefault(0)?.Split('[', 2); + var key = keyWithIndex?.ElementAtOrDefault(0); + var index = keyWithIndex?.ElementAtOrDefault(1); // with traling ']', eg: "3]" + var rest = keyParts?.ElementAtOrDefault(1); + + var properties = data?.GetProperties(); + var property = properties is not null ? Array.Find(properties,p => p.Name == key) : null; + var jsonPropertyName = property + ?.GetCustomAttributes(true) + .OfType() + .FirstOrDefault() + ?.Name; + if (jsonPropertyName is null) + { + jsonPropertyName = key; + } + + if (index is not null) + { + jsonPropertyName = jsonPropertyName + '[' + index; + } + + if (rest is null) + { + return jsonPropertyName; + } + + var childType = property?.PropertyType; + + // Get the Parameter of IEnumerable properties, if they are not string + if (childType is not null && childType != typeof(string) && childType.IsAssignableTo(typeof(IEnumerable))) + { + childType = childType.GetInterfaces() + .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + .Select(t => t.GetGenericArguments()[0]).FirstOrDefault(); + } + + if (childType is null) + { + // Give up and return rest, if the child type is not found. + return $"{jsonPropertyName}.{rest}"; + } + + return $"{jsonPropertyName}.{ModelKeyToField(rest, childType)}"; + } + + /// + /// Same as , but without information about a specific field + /// used by + /// + public static List MapModelStateToIssueList(ModelStateDictionary modelState, Instance instance, + GeneralSettings generalSettings) + { + var validationIssues = new List(); + + foreach (var modelKey in modelState.Keys) + { + modelState.TryGetValue(modelKey, out var entry); + + if (entry != null && entry.ValidationState == ModelValidationState.Invalid) + { + foreach (var error in entry.Errors) + { + var severityAndMessage = GetSeverityFromMessage(error.ErrorMessage, generalSettings); + validationIssues.Add(new ValidationIssue + { + Code = severityAndMessage.Message, + Severity = severityAndMessage.Severity, + Description = severityAndMessage.Message + }); + } + } + } + + return validationIssues; + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/IValidation.cs b/src/Altinn.App.Core/Features/Validation/IValidation.cs deleted file mode 100644 index 78c54f791..000000000 --- a/src/Altinn.App.Core/Features/Validation/IValidation.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Altinn.App.Core.Models.Validation; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Features.Validation -{ - /// - /// Describes the public methods of a validation service - /// - public interface IValidation - { - /// - /// Validate an instance for a specified process step. - /// - /// The instance to validate - /// The task to validate the instance for. - /// A list of validation errors if any were found - Task> ValidateAndUpdateProcess(Instance instance, string taskId); - - /// - /// Validate a specific data element. - /// - /// The instance where the data element belong - /// The datatype describing the data element requirements - /// The metadata of a data element to validate - /// A list of validation errors if any were found - Task> ValidateDataElement(Instance instance, DataType dataType, DataElement dataElement); - } -} diff --git a/src/Altinn.App.Core/Features/Validation/IValidationService.cs b/src/Altinn.App.Core/Features/Validation/IValidationService.cs new file mode 100644 index 000000000..1a345b0b7 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/IValidationService.cs @@ -0,0 +1,54 @@ +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Validation; + +/// +/// Core interface for validation of instances. Only a single implementation of this interface should exist in the app. +/// +public interface IValidationService +{ + /// + /// Validates the instance with all data elements on the current task and ensures that the instance is ready for process next. + /// + /// + /// This method executes validations in the following interfaces + /// * for the current task + /// * for all data elements on the current task + /// * for all data elements with app logic on the current task + /// + /// The instance to validate + /// instance.Process?.CurrentTask?.ElementId + /// List of validation issues for this data element + Task> ValidateInstanceAtTask(Instance instance, string taskId); + + /// + /// + /// + /// + /// This method executes validations in the following interfaces + /// * for all data elements on the current task + /// * for all data elements with app logic on the current task + /// + /// This method does not run task validations + /// + /// The instance to validate + /// The data element to run validations for + /// The data type (from applicationmetadata) that the element is an instance of + /// List of validation issues for this data element + Task> ValidateDataElement(Instance instance, DataElement dataElement, DataType dataType); + + /// + /// Validates a single data element. Used by frontend to continuously validate form data as it changes. + /// + /// + /// This method executes validations for + /// + /// The instance to validate + /// The data element to run validations for + /// The type of the data element + /// The data deserialized to the strongly typed object that represents the form data + /// The previous data so that validators can know if they need to run again with + /// List validators that should not be run (for incremental validation). Typically known validators that frontend knows how to replicate + Task>> ValidateFormData(Instance instance, DataElement dataElement, DataType dataType, object data, object? previousData = null, List? ignoredValidators = null); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/NullInstanceValidator.cs b/src/Altinn.App.Core/Features/Validation/NullInstanceValidator.cs deleted file mode 100644 index 4e3919be4..000000000 --- a/src/Altinn.App.Core/Features/Validation/NullInstanceValidator.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Altinn.Platform.Storage.Interface.Models; -using Microsoft.AspNetCore.Mvc.ModelBinding; - -namespace Altinn.App.Core.Features.Validation; - -/// -/// Default implementation of the IInstanceValidator interface. -/// This implementation does not do any validation and always returns true. -/// -public class NullInstanceValidator: IInstanceValidator -{ - /// - public async Task ValidateData(object data, ModelStateDictionary validationResults) - { - await Task.CompletedTask; - } - - /// - public async Task ValidateTask(Instance instance, string taskId, ModelStateDictionary validationResults) - { - await Task.CompletedTask; - } -} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs b/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs deleted file mode 100644 index 0f90de627..000000000 --- a/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs +++ /dev/null @@ -1,417 +0,0 @@ -using Altinn.App.Core.Configuration; -using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; -using Altinn.App.Core.Internal.App; -using Altinn.App.Core.Internal.AppModel; -using Altinn.App.Core.Internal.Expressions; -using Altinn.App.Core.Models.Validation; -using Altinn.Platform.Storage.Interface.Enums; -using Altinn.Platform.Storage.Interface.Models; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Altinn.App.Core.Features.Validation -{ - /// - /// Represents a validation service for validating instances and their data elements - /// - public class ValidationAppSI : IValidation - { - private readonly ILogger _logger; - private readonly IData _dataService; - private readonly IInstance _instanceService; - private readonly IInstanceValidator _instanceValidator; - private readonly IAppModel _appModel; - private readonly IAppResources _appResourcesService; - private readonly IAppMetadata _appMetadata; - private readonly LayoutEvaluatorStateInitializer _layoutEvaluatorStateInitializer; - private readonly IObjectModelValidator _objectModelValidator; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly GeneralSettings _generalSettings; - private readonly AppSettings _appSettings; - - /// - /// Initializes a new instance of the class. - /// - public ValidationAppSI( - ILogger logger, - IData dataService, - IInstance instanceService, - IInstanceValidator instanceValidator, - IAppModel appModel, - IAppResources appResourcesService, - IAppMetadata appMetadata, - IObjectModelValidator objectModelValidator, - LayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, - IHttpContextAccessor httpContextAccessor, - IOptions generalSettings, - IOptions appSettings) - { - _logger = logger; - _dataService = dataService; - _instanceService = instanceService; - _instanceValidator = instanceValidator; - _appModel = appModel; - _appResourcesService = appResourcesService; - _appMetadata = appMetadata; - _objectModelValidator = objectModelValidator; - _layoutEvaluatorStateInitializer = layoutEvaluatorStateInitializer; - _httpContextAccessor = httpContextAccessor; - _generalSettings = generalSettings.Value; - _appSettings = appSettings.Value; - } - - /// - /// Validate an instance for a specified process step. - /// - /// The instance to validate - /// The task to validate the instance for. - /// A list of validation errors if any were found - public async Task> ValidateAndUpdateProcess(Instance instance, string taskId) - { - _logger.LogInformation("Validation of {instance.Id}", instance.Id); - - List messages = new List(); - - ModelStateDictionary validationResults = new ModelStateDictionary(); - await _instanceValidator.ValidateTask(instance, taskId, validationResults); - messages.AddRange(MapModelStateToIssueList(validationResults, instance)); - - Application application = await _appMetadata.GetApplicationMetadata(); - - foreach (DataType dataType in application.DataTypes.Where(et => et.TaskId == taskId)) - { - List elements = instance.Data.Where(d => d.DataType == dataType.Id).ToList(); - - if (dataType.MaxCount > 0 && dataType.MaxCount < elements.Count) - { - ValidationIssue message = new ValidationIssue - { - InstanceId = instance.Id, - Code = ValidationIssueCodes.InstanceCodes.TooManyDataElementsOfType, - Severity = ValidationIssueSeverity.Error, - Description = ValidationIssueCodes.InstanceCodes.TooManyDataElementsOfType, - Field = dataType.Id - }; - messages.Add(message); - } - - if (dataType.MinCount > 0 && dataType.MinCount > elements.Count) - { - ValidationIssue message = new ValidationIssue - { - InstanceId = instance.Id, - Code = ValidationIssueCodes.InstanceCodes.TooFewDataElementsOfType, - Severity = ValidationIssueSeverity.Error, - Description = ValidationIssueCodes.InstanceCodes.TooFewDataElementsOfType, - Field = dataType.Id - }; - messages.Add(message); - } - - foreach (DataElement dataElement in elements) - { - messages.AddRange(await ValidateDataElement(instance, dataType, dataElement)); - } - } - - instance.Process.CurrentTask.Validated = new ValidationStatus - { - // The condition for completion is met if there are no errors (or other weirdnesses). - CanCompleteTask = messages.Count == 0 || - messages.All(m => m.Severity != ValidationIssueSeverity.Error && m.Severity != ValidationIssueSeverity.Unspecified), - Timestamp = DateTime.Now - }; - - await _instanceService.UpdateProcess(instance); - return messages; - } - - /// - /// Validate a specific data element. - /// - /// The instance where the data element belong - /// The datatype describing the data element requirements - /// The metadata of a data element to validate - /// A list of validation errors if any were found - public async Task> ValidateDataElement(Instance instance, DataType dataType, DataElement dataElement) - { - _logger.LogInformation("Validation of data element {dataElement.Id} of instance {instance.Id}", dataElement.Id, instance.Id); - - List messages = new List(); - - if (dataElement.ContentType == null) - { - ValidationIssue message = new ValidationIssue - { - InstanceId = instance.Id, - Code = ValidationIssueCodes.DataElementCodes.MissingContentType, - DataElementId = dataElement.Id, - Severity = ValidationIssueSeverity.Error, - Description = ValidationIssueCodes.DataElementCodes.MissingContentType - }; - messages.Add(message); - } - else - { - string contentTypeWithoutEncoding = dataElement.ContentType.Split(";")[0]; - - if (dataType.AllowedContentTypes != null && dataType.AllowedContentTypes.Count > 0 && dataType.AllowedContentTypes.All(ct => !ct.Equals(contentTypeWithoutEncoding, StringComparison.OrdinalIgnoreCase))) - { - ValidationIssue message = new ValidationIssue - { - InstanceId = instance.Id, - DataElementId = dataElement.Id, - Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, - Severity = ValidationIssueSeverity.Error, - Description = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, - Field = dataType.Id - }; - messages.Add(message); - } - } - - if (dataType.MaxSize.HasValue && dataType.MaxSize > 0 && (long)dataType.MaxSize * 1024 * 1024 < dataElement.Size) - { - ValidationIssue message = new ValidationIssue - { - InstanceId = instance.Id, - DataElementId = dataElement.Id, - Code = ValidationIssueCodes.DataElementCodes.DataElementTooLarge, - Severity = ValidationIssueSeverity.Error, - Description = ValidationIssueCodes.DataElementCodes.DataElementTooLarge, - Field = dataType.Id - }; - messages.Add(message); - } - - if (dataType.EnableFileScan && dataElement.FileScanResult == FileScanResult.Infected) - { - ValidationIssue message = new ValidationIssue() - { - InstanceId = instance.Id, - DataElementId = dataElement.Id, - Code = ValidationIssueCodes.DataElementCodes.DataElementFileInfected, - Severity = ValidationIssueSeverity.Error, - Description = ValidationIssueCodes.DataElementCodes.DataElementFileInfected, - Field = dataType.Id - }; - messages.Add(message); - } - - if (dataType.EnableFileScan && dataType.ValidationErrorOnPendingFileScan && dataElement.FileScanResult == FileScanResult.Pending) - { - ValidationIssue message = new ValidationIssue() - { - InstanceId = instance.Id, - DataElementId = dataElement.Id, - Code = ValidationIssueCodes.DataElementCodes.DataElementFileScanPending, - Severity = ValidationIssueSeverity.Error, - Description = ValidationIssueCodes.DataElementCodes.DataElementFileScanPending, - Field = dataType.Id - }; - messages.Add(message); - } - - if (dataType.AppLogic?.ClassRef != null) - { - Type modelType = _appModel.GetModelType(dataType.AppLogic.ClassRef); - Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); - string app = instance.AppId.Split("/")[1]; - int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); - object data = await _dataService.GetFormData( - instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, Guid.Parse(dataElement.Id)); - - if (_appSettings.RemoveHiddenDataPreview) - { - var layoutSet = _appResourcesService.GetLayoutSetForTask(dataType.TaskId); - var evaluationState = await _layoutEvaluatorStateInitializer.Init(instance, data, layoutSet?.Id); - // Remove hidden data before validation, set rows to null to preserve indices - LayoutEvaluator.RemoveHiddenData(evaluationState, RowRemovalOption.SetToNull); - // Evaluate expressions in layout and validate that all required data is included and that maxLength - // is respected on groups - var layoutErrors = LayoutEvaluator.RunLayoutValidationsForRequired(evaluationState, dataElement.Id); - messages.AddRange(layoutErrors); - } - - // Run Standard mvc validation using the System.ComponentModel.DataAnnotations - ModelStateDictionary dataModelValidationResults = new ModelStateDictionary(); - var actionContext = new ActionContext( - _httpContextAccessor.HttpContext, - new Microsoft.AspNetCore.Routing.RouteData(), - new ActionDescriptor(), - dataModelValidationResults); - ValidationStateDictionary validationState = new ValidationStateDictionary(); - _objectModelValidator.Validate(actionContext, validationState, null, data); - - if (!dataModelValidationResults.IsValid) - { - messages.AddRange(MapModelStateToIssueList(actionContext.ModelState, ValidationIssueSources.ModelState, instance, dataElement.Id, data.GetType())); - } - - // Call custom validation from the IInstanceValidator - ModelStateDictionary customValidationResults = new ModelStateDictionary(); - await _instanceValidator.ValidateData(data, customValidationResults); - - if (!customValidationResults.IsValid) - { - messages.AddRange(MapModelStateToIssueList(customValidationResults, ValidationIssueSources.Custom, instance, dataElement.Id, data.GetType())); - } - - } - - return messages; - } - - private List MapModelStateToIssueList( - ModelStateDictionary modelState, - string source, - Instance instance, - string dataElementId, - Type modelType) - { - List validationIssues = new List(); - - foreach (string modelKey in modelState.Keys) - { - modelState.TryGetValue(modelKey, out ModelStateEntry? entry); - - if (entry != null && entry.ValidationState == ModelValidationState.Invalid) - { - foreach (ModelError error in entry.Errors) - { - var severityAndMessage = GetSeverityFromMessage(error.ErrorMessage); - validationIssues.Add(new ValidationIssue - { - InstanceId = instance.Id, - DataElementId = dataElementId, - Source = source, - Code = severityAndMessage.Message, - Field = ModelKeyToField(modelKey, modelType)!, - Severity = severityAndMessage.Severity, - Description = severityAndMessage.Message - }); - } - } - } - - return validationIssues; - } - - /// - /// Translate the ModelKey from validation to a field that respects [JsonPropertyName] annotations - /// - /// - /// Will be obsolete when updating to net70 or higher and activating https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-7.0#use-json-property-names-in-validation-errors - /// - public static string? ModelKeyToField(string? modelKey, Type data) - { - var keyParts = modelKey?.Split('.', 2); - var keyWithIndex = keyParts?.ElementAtOrDefault(0)?.Split('[', 2); - var key = keyWithIndex?.ElementAtOrDefault(0); - var index = keyWithIndex?.ElementAtOrDefault(1); // with traling ']', eg: "3]" - var rest = keyParts?.ElementAtOrDefault(1); - - var property = data?.GetProperties()?.FirstOrDefault(p => p.Name == key); - var jsonPropertyName = property - ?.GetCustomAttributes(true) - .OfType() - .FirstOrDefault() - ?.Name; - if (jsonPropertyName is null) - { - jsonPropertyName = key; - } - - if (index is not null) - { - jsonPropertyName = jsonPropertyName + '[' + index; - } - - if (rest is null) - { - return jsonPropertyName; - } - - var childType = property?.PropertyType; - - // Get the Parameter of IEnumerable properties, if they are not string - if (childType is not null && childType != typeof(string) && childType.IsAssignableTo(typeof(System.Collections.IEnumerable))) - { - childType = childType.GetInterfaces() - .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - .Select(t => t.GetGenericArguments()[0]).FirstOrDefault(); - } - - if (childType is null) - { - // Give up and return rest, if the child type is not found. - return $"{jsonPropertyName}.{rest}"; - } - - return $"{jsonPropertyName}.{ModelKeyToField(rest, childType)}"; - } - - private List MapModelStateToIssueList(ModelStateDictionary modelState, Instance instance) - { - List validationIssues = new List(); - - foreach (string modelKey in modelState.Keys) - { - modelState.TryGetValue(modelKey, out ModelStateEntry? entry); - - if (entry != null && entry.ValidationState == ModelValidationState.Invalid) - { - foreach (ModelError error in entry.Errors) - { - var severityAndMessage = GetSeverityFromMessage(error.ErrorMessage); - validationIssues.Add(new ValidationIssue - { - InstanceId = instance.Id, - Code = severityAndMessage.Message, - Severity = severityAndMessage.Severity, - Description = severityAndMessage.Message - }); - } - } - } - - return validationIssues; - } - - private (ValidationIssueSeverity Severity, string Message) GetSeverityFromMessage(string originalMessage) - { - if (originalMessage.StartsWith(_generalSettings.SoftValidationPrefix)) - { - return (ValidationIssueSeverity.Warning, - originalMessage.Remove(0, _generalSettings.SoftValidationPrefix.Length)); - } - - if (_generalSettings.FixedValidationPrefix != null - && originalMessage.StartsWith(_generalSettings.FixedValidationPrefix)) - { - return (ValidationIssueSeverity.Fixed, - originalMessage.Remove(0, _generalSettings.FixedValidationPrefix.Length)); - } - - if (originalMessage.StartsWith(_generalSettings.InfoValidationPrefix)) - { - return (ValidationIssueSeverity.Informational, - originalMessage.Remove(0, _generalSettings.InfoValidationPrefix.Length)); - } - - if (originalMessage.StartsWith(_generalSettings.SuccessValidationPrefix)) - { - return (ValidationIssueSeverity.Success, - originalMessage.Remove(0, _generalSettings.SuccessValidationPrefix.Length)); - } - - return (ValidationIssueSeverity.Error, originalMessage); - } - } -} diff --git a/src/Altinn.App.Core/Features/Validation/ValidationService.cs b/src/Altinn.App.Core/Features/Validation/ValidationService.cs new file mode 100644 index 000000000..4dbff6bca --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/ValidationService.cs @@ -0,0 +1,156 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Altinn.App.Core.Features.Validation; + +/// +/// Main validation service that encapsulates all validation logic +/// +public class ValidationService : IValidationService +{ + private readonly IServiceProvider _serviceProvider; + private readonly IDataClient _dataClient; + private readonly IAppModel _appModel; + private readonly IAppMetadata _appMetadata; + private readonly ILogger _logger; + + /// + /// Constructor with DI services + /// + public ValidationService(IServiceProvider serviceProvider, IDataClient dataClient, IAppModel appModel, IAppMetadata appMetadata, ILogger logger) + { + _serviceProvider = serviceProvider; + _dataClient = dataClient; + _appModel = appModel; + _appMetadata = appMetadata; + _logger = logger; + } + + /// + public async Task> ValidateInstanceAtTask(Instance instance, string taskId) + { + ArgumentNullException.ThrowIfNull(instance); + ArgumentNullException.ThrowIfNull(taskId); + + // Run task validations + var taskValidators = _serviceProvider.GetServices() + .Where(tv => tv.TaskId == "*" || tv.TaskId == taskId) + // .Concat(_serviceProvider.GetKeyedServices(taskId)) + .ToArray(); + + var taskIssuesTask = Task.WhenAll(taskValidators.Select(async tv => + { + try + { + _logger.LogDebug("Start running validator {validatorName} on task {taskId} in instance {instanceId}", tv.GetType().Name, taskId, instance.Id); + var issues = await tv.ValidateTask(instance, taskId); + issues.ForEach(i => i.Source = tv.ValidationSource); // Ensure that the source is set to the validator source + return issues; + } + catch (Exception e) + { + _logger.LogError(e, "Error while running validator {validatorName} on task {taskId} in instance {instanceId}", tv.GetType().Name, taskId, instance.Id); + throw; + } + })); + + // Run validations for single data elements + var application = await _appMetadata.GetApplicationMetadata(); + var dataTypesForTask = application.DataTypes.Where(dt => dt.TaskId == taskId).ToList(); + var dataElementsToValidate = instance.Data.Where(de => dataTypesForTask.Exists(dt => dt.Id == de.DataType)).ToArray(); + var dataIssuesTask = Task.WhenAll(dataElementsToValidate.Select(dataElement=>ValidateDataElement(instance, dataElement, dataTypesForTask.First(dt=>dt.Id == dataElement.DataType) ))); + + return (await Task.WhenAll(taskIssuesTask, dataIssuesTask)).SelectMany(x=>x.SelectMany(y=>y)).ToList(); + } + + + /// + public async Task> ValidateDataElement(Instance instance, DataElement dataElement, DataType dataType) + { + ArgumentNullException.ThrowIfNull(instance); + ArgumentNullException.ThrowIfNull(dataElement); + ArgumentNullException.ThrowIfNull(dataElement.DataType); + + // Get both keyed and non-keyed validators for the data type + var validators = _serviceProvider.GetServices() + .Where(v => v.DataType == "*" || v.DataType == dataType.Id) + // .Concat(_serviceProvider.GetKeyedServices(dataElement.DataType)) + .ToArray(); + + var dataElementsIssuesTask = Task.WhenAll(validators.Select(async v => + { + try + { + _logger.LogDebug("Start running validator {validatorName} on {dataType} for data element {dataElementId} in instance {instanceId}", v.GetType().Name, dataElement.DataType, dataElement.Id, instance.Id); + var issues = await v.ValidateDataElement(instance, dataElement, dataType); + issues.ForEach(i => i.Source = v.ValidationSource); // Ensure that the source is set to the validator source + return issues; + } + catch (Exception e) + { + _logger.LogError(e, "Error while running validator {validatorName} on {dataType} for data element {dataElementId} in instance {instanceId}", v.GetType().Name, dataElement.DataType, dataElement.Id, instance.Id); + throw; + } + })); + + // Run extra validation on form data elements with app logic + if(dataType.AppLogic?.ClassRef is not null) + { + Type modelType = _appModel.GetModelType(dataType.AppLogic.ClassRef); + + Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); + string app = instance.AppId.Split("/")[1]; + int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); + var data = await _dataClient.GetFormData(instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, Guid.Parse(dataElement.Id)); // TODO: Add method that accepts instance and dataElement + var formDataIssuesDictionary = await ValidateFormData(instance, dataElement, dataType, data); + + return (await dataElementsIssuesTask).SelectMany(x=>x) + .Concat(formDataIssuesDictionary.SelectMany(kv=>kv.Value)) + .ToList(); + } + + return (await dataElementsIssuesTask).SelectMany(x=>x).ToList(); + } + + /// + public async Task>> ValidateFormData(Instance instance, DataElement dataElement, DataType dataType, object data, + object? previousData = null, List? ignoredValidators = null) + { + ArgumentNullException.ThrowIfNull(instance); + ArgumentNullException.ThrowIfNull(dataElement); + ArgumentNullException.ThrowIfNull(dataElement.DataType); + ArgumentNullException.ThrowIfNull(data); + + // Locate the relevant data validator services from normal and keyed services + var dataValidators = _serviceProvider.GetServices() + .Where(dv => dv.DataType == "*" || dv.DataType == dataType.Id) + // .Concat(_serviceProvider.GetKeyedServices(dataElement.DataType)) + .Where(dv => ignoredValidators?.Contains(dv.ValidationSource) != true) + .Where(dv => previousData is null || dv.HasRelevantChanges(data, previousData)) + .ToArray(); + + var issuesLists = await Task.WhenAll(dataValidators.Select(async (v) => + { + try + { + _logger.LogDebug("Start running validator {validatorName} on {dataType} for data element {dataElementId} in instance {instanceId}", v.GetType().Name, dataElement.DataType, dataElement.Id, instance.Id); + var issues = await v.ValidateFormData(instance, dataElement, data); + issues.ForEach(i => i.Source = v.ValidationSource);// Ensure that the code is set to the validator code + return issues; + } + catch (Exception e) + { + _logger.LogError(e, "Error while running validator {validatorName} on {dataType} for data element {dataElementId} in instance {instanceId}", v.GetType().Name, dataElement.DataType, dataElement.Id, instance.Id); + throw; + } + })); + + return dataValidators.Zip(issuesLists).ToDictionary(kv => kv.First.ValidationSource, kv => kv.Second); + } + +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs b/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs index 9aed92f8d..ec45626c9 100644 --- a/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs +++ b/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs @@ -90,6 +90,80 @@ public DataModel(object serviceModel) return GetModelDataRecursive(keys, index + 1, elementAt, indicies.Length > 0 ? indicies.Slice(1) : indicies); } + /// + public string[] GetResolvedKeys(string key) + { + if (_serviceModel is null) + { + return new string[0]; + } + + var keyParts = key.Split('.'); + return GetResolvedKeysRecursive(keyParts, _serviceModel); + } + + internal static string JoinFieldKeyParts(string? currentKey, string? key) + { + if (String.IsNullOrEmpty(currentKey)) + { + return key ?? ""; + } + if (String.IsNullOrEmpty(key)) + { + return currentKey ?? ""; + } + + return currentKey + "." + key; + } + + private string[] GetResolvedKeysRecursive(string[] keyParts, object currentModel, int currentIndex = 0, string currentKey = "") + { + if (currentModel is null) + { + return new string[0]; + } + + if (currentIndex == keyParts.Length) + { + return new[] { currentKey }; + } + + var (key, groupIndex) = ParseKeyPart(keyParts[currentIndex]); + var prop = currentModel?.GetType().GetProperties().FirstOrDefault(p => IsPropertyWithJsonName(p, key)); + var childModel = prop?.GetValue(currentModel); + if (childModel is null) + { + return new string[0]; + } + + if (childModel is not string && childModel is System.Collections.IEnumerable childModelList) + { + // childModel is an array + if (groupIndex is null) + { + // Index not specified, recurse on all elements + int i = 0; + var resolvedKeys = new List(); + foreach (var child in childModelList) + { + var newResolvedKeys = GetResolvedKeysRecursive(keyParts, child, currentIndex + 1, JoinFieldKeyParts(currentKey, key + "[" + i + "]")); + resolvedKeys.AddRange(newResolvedKeys); + i++; + } + return resolvedKeys.ToArray(); + } + else + { + // Index specified, recurse on that element + return GetResolvedKeysRecursive(keyParts, childModel, currentIndex + 1, JoinFieldKeyParts(currentKey, key + "[" + groupIndex + "]")); + } + } + + // Otherwise, just recurse + return GetResolvedKeysRecursive(keyParts, childModel, currentIndex + 1, JoinFieldKeyParts(currentKey, key)); + + } + private static object? GetElementAt(System.Collections.IEnumerable enumerable, int index) { // Return the element with index = groupIndex (could not find anohter way to get the n'th element in non generic enumerable) diff --git a/src/Altinn.App.Core/Helpers/DataModel/DataModelException.cs b/src/Altinn.App.Core/Helpers/DataModel/DataModelException.cs index c00ff9634..6aa37e379 100644 --- a/src/Altinn.App.Core/Helpers/DataModel/DataModelException.cs +++ b/src/Altinn.App.Core/Helpers/DataModel/DataModelException.cs @@ -5,12 +5,8 @@ namespace Altinn.App.Core.Helpers.DataModel; /// /// Custom exception for errors when reading from a datamodel /// -[Serializable] public class DataModelException : Exception { /// public DataModelException(string msg): base(msg) { } - - /// - protected DataModelException(SerializationInfo info, StreamingContext ctxt) : base(info, ctxt) { } } \ No newline at end of file diff --git a/src/Altinn.App.Core/Helpers/IDataModel.cs b/src/Altinn.App.Core/Helpers/IDataModel.cs index 2f267da9e..9d720b1aa 100644 --- a/src/Altinn.App.Core/Helpers/IDataModel.cs +++ b/src/Altinn.App.Core/Helpers/IDataModel.cs @@ -26,6 +26,11 @@ public interface IDataModelAccessor /// int? GetModelDataCount(string key, ReadOnlySpan indicies = default); + /// + /// Get all of the resoved keys (including all possible indexes) from a data model key + /// + string[] GetResolvedKeys(string key); + /// /// Return a full dataModelBiding from a context aware binding by adding indicies /// diff --git a/src/Altinn.App.Core/Helpers/JsonHelper.cs b/src/Altinn.App.Core/Helpers/JsonHelper.cs index 075e9d7b3..538312165 100644 --- a/src/Altinn.App.Core/Helpers/JsonHelper.cs +++ b/src/Altinn.App.Core/Helpers/JsonHelper.cs @@ -16,33 +16,31 @@ public static class JsonHelper /// /// Run DataProcessWrite returning the dictionary of the changed fields. /// - public static async Task?> ProcessDataWriteWithDiff(Instance instance, Guid dataGuid, object serviceModel, IDataProcessor dataProcessor, ILogger logger) + public static async Task?> ProcessDataWriteWithDiff(Instance instance, Guid dataGuid, object serviceModel, IEnumerable dataProcessors, ILogger logger) { - string serviceModelJsonString = System.Text.Json.JsonSerializer.Serialize(serviceModel); - - bool changedByCalculation = await dataProcessor.ProcessDataWrite(instance, dataGuid, serviceModel); - - Dictionary? changedFields = null; - if (changedByCalculation) + if (!dataProcessors.Any()) { - string updatedServiceModelString = System.Text.Json.JsonSerializer.Serialize(serviceModel); - try - { - changedFields = FindChangedFields(serviceModelJsonString, updatedServiceModelString); - } - catch (Exception e) - { - logger.LogError(e, "Unable to determine changed fields"); - } + return null; } - // TODO: Consider not bothering frontend with an empty changes list - // if(changedFields?.Count == 0) - // { - // return null; - // } + string serviceModelJsonString = System.Text.Json.JsonSerializer.Serialize(serviceModel); + foreach (var dataProcessor in dataProcessors) + { + logger.LogInformation("ProcessDataRead for {modelType} using {dataProcesor}", serviceModel.GetType().Name, dataProcessor.GetType().Name); + await dataProcessor.ProcessDataWrite(instance, dataGuid, serviceModel, null); + } - return changedFields; + string updatedServiceModelString = System.Text.Json.JsonSerializer.Serialize(serviceModel); + try + { + var changed = FindChangedFields(serviceModelJsonString, updatedServiceModelString); + return changed.Count == 0 ? null : changed; + } + catch (Exception e) + { + logger.LogError(e, "Unable to determine changed fields"); + return null; + } } /// diff --git a/src/Altinn.App.Core/Helpers/JsonSerializerPermissive.cs b/src/Altinn.App.Core/Helpers/JsonSerializerPermissive.cs new file mode 100644 index 000000000..e0603dbfb --- /dev/null +++ b/src/Altinn.App.Core/Helpers/JsonSerializerPermissive.cs @@ -0,0 +1,49 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.Platform.Register.Models; + +namespace Altinn.App.Core.Helpers; + +/// +/// Wrapper of with permissive settings parsing settings. +/// +public static class JsonSerializerPermissive +{ + /// + /// for the most permissive parsing of JSON. + /// + public static readonly JsonSerializerOptions JsonSerializerOptionsDefaults = new(JsonSerializerDefaults.Web) + { + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip, + Converters = + { + new JsonStringEnumConverter(), + }, + }; + + /// + /// Simple wrapper of with permissive defaults. + /// + public static T Deserialize(string content) + { + return JsonSerializer.Deserialize(content, JsonSerializerOptionsDefaults) ?? throw new JsonException("Could not deserialize json value \"null\" to type " + typeof(T).FullName); + } + + /// + /// Simple wrapper of with permissive defaults. + /// + public static async Task DeserializeAsync(HttpContent content, CancellationToken cancellationToken = default) + { + await using var stream = await content.ReadAsStreamAsync(cancellationToken); + return await JsonSerializer.DeserializeAsync(stream, JsonSerializerOptionsDefaults, cancellationToken) ?? throw new JsonException("Could not deserialize json value \"null\" to type " + typeof(T).FullName); + } + + /// + /// Simple wrapper of with permissive defaults. + /// + public static string Serialize(PartyLookup partyLookup) + { + return JsonSerializer.Serialize(partyLookup, JsonSerializerOptionsDefaults); + } +} diff --git a/src/Altinn.App.Core/Helpers/LinqExpressionHelpers.cs b/src/Altinn.App.Core/Helpers/LinqExpressionHelpers.cs new file mode 100644 index 000000000..406f815f4 --- /dev/null +++ b/src/Altinn.App.Core/Helpers/LinqExpressionHelpers.cs @@ -0,0 +1,83 @@ +using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json.Serialization; + +namespace Altinn.App.Core.Helpers; + +/// +/// Utilities for working with +/// +public static class LinqExpressionHelpers +{ + /// + /// Gets the JSON path from an expression + /// + /// The expression + /// The JSON path + public static string GetJsonPath(Expression> expression) + { + return GetJsonPath_internal(expression); + } + + /// + /// Need a private method to avoid the generic type parameter for recursion + /// + private static string GetJsonPath_internal(Expression expression) + { + ArgumentNullException.ThrowIfNull(expression); + + var path = new List(); + Expression? current = expression; + while (current is not null) + { + switch (current) + { + case MemberExpression memberExpression: + path.Add(GetJsonPropertyName(memberExpression.Member)); + current = memberExpression.Expression; + break; + case LambdaExpression lambdaExpression: + current = lambdaExpression.Body; + break; + case ParameterExpression: + // We have reached the root of the expression + current = null; + break; + + // This is a special case for accessing a list item by index + case MethodCallExpression { Method.Name: "get_Item", Arguments: [ ConstantExpression { Value: Int32 index } ], Object: MemberExpression memberExpression }: + path.Add($"{GetJsonPropertyName(memberExpression.Member)}[{index}]"); + current = memberExpression.Expression; + break; + // This is a special case for accessing a list item by index in a variable + case MethodCallExpression { Method.Name: "get_Item", Arguments: [ MemberExpression { Expression: ConstantExpression constantExpression, Member: FieldInfo fieldInfo }], Object: MemberExpression memberExpression }: + // Evaluate the constant expression to get the index + var evaluatedIndex = fieldInfo.GetValue(constantExpression.Value); + path.Add($"{GetJsonPropertyName(memberExpression.Member)}[{evaluatedIndex}]"); + current = memberExpression.Expression; + break; + // This is a special case for selecting all childern of a list using Select + case MethodCallExpression { Method.Name: "Select" } methodCallExpression: + path.Add(GetJsonPath_internal(methodCallExpression.Arguments[1])); + current = methodCallExpression.Arguments[0]; + break; + default: + throw new ArgumentException($"Invalid expression {expression}. Failed reading {current}"); + } + } + + path.Reverse(); + return string.Join(".", path); + } + + private static string GetJsonPropertyName(MemberInfo memberExpressionMember) + { + var jsonPropertyAttribute = memberExpressionMember.GetCustomAttribute(); + if (jsonPropertyAttribute is not null) + { + return jsonPropertyAttribute.Name; + } + + return memberExpressionMember.Name; + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Helpers/MultiDecisionHelper.cs b/src/Altinn.App.Core/Helpers/MultiDecisionHelper.cs new file mode 100644 index 000000000..931393556 --- /dev/null +++ b/src/Altinn.App.Core/Helpers/MultiDecisionHelper.cs @@ -0,0 +1,185 @@ +using System.Security.Claims; +using Altinn.App.Core.Models; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Constants; +using Altinn.Common.PEP.Helpers; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Helpers; + +/// +/// Helper class for multi decision requests. +/// +public static class MultiDecisionHelper +{ + private const string XacmlResourceTaskId = "urn:altinn:task"; + private const string XacmlResourceEndId = "urn:altinn:end-event"; + private const string XacmlResourceActionId = "urn:oasis:names:tc:xacml:1.0:action:action-id"; + private const string DefaultIssuer = "Altinn"; + private const string DefaultType = "string"; + private const string SubjectId = "s"; + private const string ActionId = "a"; + private const string ResourceId = "r"; + + /// + /// Creates multi decision request. + /// + public static XacmlJsonRequestRoot CreateMultiDecisionRequest(ClaimsPrincipal user, Instance instance, List actionTypes) + { + ArgumentNullException.ThrowIfNull(user); + + XacmlJsonRequest request = new() + { + AccessSubject = new List() + }; + + request.AccessSubject.Add(CreateMultipleSubjectCategory(user.Claims)); + request.Action = CreateMultipleActionCategory(actionTypes); + request.Resource = CreateMultipleResourceCategory(instance); + request.MultiRequests = CreateMultiRequestsCategory(request.AccessSubject, request.Action, request.Resource); + + XacmlJsonRequestRoot jsonRequest = new() { Request = request }; + + return jsonRequest; + } + + /// + /// Validate a multi decision result and returns a dictionary with the actions and the result. + /// + /// + /// + /// + /// + /// + public static Dictionary ValidatePdpMultiDecision(Dictionary actions, List results, ClaimsPrincipal user) + { + ArgumentNullException.ThrowIfNull(results); + ArgumentNullException.ThrowIfNull(user); + foreach (XacmlJsonResult result in results.Where(r => DecisionHelper.ValidateDecisionResult(r, user))) + { + foreach (var attributes in result.Category.Select(c => c.Attribute)) + { + foreach (var attribute in attributes) + { + if (attribute.AttributeId == XacmlResourceActionId) + { + actions[attribute.Value] = true; + } + } + } + } + + return actions; + } + + private static XacmlJsonCategory CreateMultipleSubjectCategory(IEnumerable claims) + { + XacmlJsonCategory subjectAttributes = DecisionHelper.CreateSubjectCategory(claims); + subjectAttributes.Id = SubjectId + "1"; + + return subjectAttributes; + } + + private static List CreateMultipleActionCategory(List actionTypes) + { + List actionCategories = new(); + int counter = 1; + + foreach (string actionType in actionTypes) + { + XacmlJsonCategory actionCategory; + actionCategory = DecisionHelper.CreateActionCategory(actionType, true); + actionCategory.Id = ActionId + counter.ToString(); + actionCategories.Add(actionCategory); + counter++; + } + + return actionCategories; + } + + private static List CreateMultipleResourceCategory(Instance instance) + { + List resourcesCategories = new(); + int counter = 1; + XacmlJsonCategory resourceCategory = new() { Attribute = new List() }; + + var instanceProps = GetInstanceProperties(instance); + + if (instanceProps.Task != null) + { + resourceCategory.Attribute.Add(DecisionHelper.CreateXacmlJsonAttribute(XacmlResourceTaskId, instanceProps.Task, DefaultType, DefaultIssuer)); + } + else if (instance.Process?.EndEvent != null) + { + resourceCategory.Attribute.Add(DecisionHelper.CreateXacmlJsonAttribute(XacmlResourceEndId, instance.Process.EndEvent, DefaultType, DefaultIssuer)); + } + + if (!string.IsNullOrWhiteSpace(instanceProps.InstanceId)) + { + resourceCategory.Attribute.Add(DecisionHelper.CreateXacmlJsonAttribute(AltinnXacmlUrns.InstanceId, instanceProps.InstanceId, DefaultType, DefaultIssuer, true)); + } + else if (!string.IsNullOrEmpty(instanceProps.InstanceGuid)) + { + resourceCategory.Attribute.Add(DecisionHelper.CreateXacmlJsonAttribute(AltinnXacmlUrns.InstanceId, instanceProps.InstanceOwnerPartyId + "/" + instanceProps.InstanceGuid, DefaultType, DefaultIssuer, true)); + } + + resourceCategory.Attribute.Add(DecisionHelper.CreateXacmlJsonAttribute(AltinnXacmlUrns.PartyId, instanceProps.InstanceOwnerPartyId, DefaultType, DefaultIssuer)); + resourceCategory.Attribute.Add(DecisionHelper.CreateXacmlJsonAttribute(AltinnXacmlUrns.OrgId, instanceProps.appIdentifier.Org, DefaultType, DefaultIssuer)); + resourceCategory.Attribute.Add(DecisionHelper.CreateXacmlJsonAttribute(AltinnXacmlUrns.AppId, instanceProps.appIdentifier.App, DefaultType, DefaultIssuer)); + resourceCategory.Id = ResourceId + counter; + resourcesCategories.Add(resourceCategory); + + return resourcesCategories; + } + + private static (string? InstanceId, string InstanceGuid, string? Task, string InstanceOwnerPartyId, AppIdentifier appIdentifier) GetInstanceProperties(Instance instance) + { + string? instanceId = instance.Id.Contains('/') ? instance.Id : null; + string instanceGuid = instance.Id.Contains('/') ? instance.Id.Split("/")[1] : instance.Id; + string? task = instance.Process?.CurrentTask?.ElementId; + string instanceOwnerPartyId = instance.InstanceOwner.PartyId; + AppIdentifier appIdentifier = new(instance); + return (instanceId, instanceGuid, task, instanceOwnerPartyId, appIdentifier); + } + + private static XacmlJsonMultiRequests CreateMultiRequestsCategory(List subjects, List actions, List resources) + { + List subjectIds = subjects.Select(s => s.Id).ToList(); + List actionIds = actions.Select(a => a.Id).ToList(); + List resourceIds = resources.Select(r => r.Id).ToList(); + + XacmlJsonMultiRequests multiRequests = new() + { + RequestReference = CreateRequestReference(subjectIds, actionIds, resourceIds) + }; + + return multiRequests; + } + + private static List CreateRequestReference(List subjectIds, List actionIds, List resourceIds) + { + List references = new(); + + foreach (string resourceId in resourceIds) + { + foreach (string actionId in actionIds) + { + foreach (string subjectId in subjectIds) + { + XacmlJsonRequestReference reference = new(); + List referenceId = new() + { + subjectId, + actionId, + resourceId + }; + reference.ReferenceId = referenceId; + references.Add(reference); + } + } + } + + return references; + } + +} diff --git a/src/Altinn.App.Core/Helpers/ObjectUtils.cs b/src/Altinn.App.Core/Helpers/ObjectUtils.cs new file mode 100644 index 000000000..30f363af7 --- /dev/null +++ b/src/Altinn.App.Core/Helpers/ObjectUtils.cs @@ -0,0 +1,54 @@ +using System.Collections; + +namespace Altinn.App.Core.Helpers; + +/// +/// Utilities for working with model instances +/// +public static class ObjectUtils +{ + /// + /// Recursively initialize all properties on the object that are currently null + /// Also ensure that all string properties that are empty are set to null + /// + /// The object to mutate + public static void InitializeListsAndNullEmptyStrings(object model) + { + foreach (var prop in model.GetType().GetProperties()) + { + if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) + { + var value = prop.GetValue(model); + if (value is null) + { + // Initialize IList with null value + prop.SetValue(model, Activator.CreateInstance(prop.PropertyType)); + } + else + { + foreach (var item in (IList)value) + { + // Recurse into values of a list + InitializeListsAndNullEmptyStrings(item); + } + } + } + else if (prop.GetIndexParameters().Length == 0) + { + var value = prop.GetValue(model); + + if (value is "") + { + // Initialize string with null value (xml serialization does not always preserve "") + prop.SetValue(model, null); + } + + // continue recursion over all properties + if (value is not null) + { + InitializeListsAndNullEmptyStrings(value); + } + } + } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Helpers/PlatformHttpException.cs b/src/Altinn.App.Core/Helpers/PlatformHttpException.cs index 16c57703f..58911cf51 100644 --- a/src/Altinn.App.Core/Helpers/PlatformHttpException.cs +++ b/src/Altinn.App.Core/Helpers/PlatformHttpException.cs @@ -5,7 +5,6 @@ namespace Altinn.App.Core.Helpers /// /// Exception class to hold exceptions when talking to the platform REST services /// - [Serializable] public class PlatformHttpException : Exception { /// @@ -36,12 +35,5 @@ public PlatformHttpException(HttpResponseMessage response, string message) : bas { this.Response = response; } - - /// - /// Add serialization info. - /// - protected PlatformHttpException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } } } diff --git a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs index 83240cb4e..7ee5921ac 100644 --- a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs +++ b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs @@ -41,13 +41,13 @@ public ModelDeserializer(ILogger logger, Type modelType) /// The data stream to deserialize. /// The content type of the stream. /// An instance of the initialized type if deserializing succeed. - public async Task DeserializeAsync(Stream stream, string contentType) + public async Task DeserializeAsync(Stream stream, string? contentType) { Error = null; if (contentType == null) { - Error = $"Unknown content type {contentType}. Cannot read the data."; + Error = $"Unknown content type \"null\". Cannot read the data."; return null; } diff --git a/src/Altinn.App.Core/Helpers/ServiceException.cs b/src/Altinn.App.Core/Helpers/ServiceException.cs index 08b2e4a59..04bfc1e3b 100644 --- a/src/Altinn.App.Core/Helpers/ServiceException.cs +++ b/src/Altinn.App.Core/Helpers/ServiceException.cs @@ -6,7 +6,6 @@ namespace Altinn.App.Core.Helpers /// /// Exception that is thrown by service implementation. /// - [Serializable] public class ServiceException : Exception { /// @@ -34,12 +33,5 @@ public ServiceException(HttpStatusCode statusCode, string message, Exception inn { StatusCode = statusCode; } - - /// - /// Set serialization info. - /// - protected ServiceException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } } } diff --git a/src/Altinn.App.Core/Helpers/UserHelper.cs b/src/Altinn.App.Core/Helpers/UserHelper.cs index e7cfe7e2b..fdbe76921 100644 --- a/src/Altinn.App.Core/Helpers/UserHelper.cs +++ b/src/Altinn.App.Core/Helpers/UserHelper.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using Altinn.App.Core.Configuration; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Altinn.Platform.Profile.Models; using AltinnCore.Authentication.Constants; @@ -14,20 +15,20 @@ namespace Altinn.App.Core.Helpers /// public class UserHelper { - private readonly IProfile _profileService; - private readonly IRegister _registerService; + private readonly IProfileClient _profileClient; + private readonly IAltinnPartyClient _altinnPartyClientService; private readonly GeneralSettings _settings; /// /// Initializes a new instance of the class /// - /// The ProfileService (defined in Startup.cs) - /// The RegisterService (defined in Startup.cs) + /// The ProfileService (defined in Startup.cs) + /// The RegisterService (defined in Startup.cs) /// The general settings - public UserHelper(IProfile profileService, IRegister registerService, IOptions settings) + public UserHelper(IProfileClient profileClient, IAltinnPartyClient altinnPartyClientService, IOptions settings) { - _profileService = profileService; - _registerService = registerService; + _profileClient = profileClient; + _altinnPartyClientService = altinnPartyClientService; _settings = settings.Value; } @@ -63,7 +64,7 @@ public async Task GetUserContext(HttpContext context) } } - UserProfile userProfile = await _profileService.GetUserProfile(userContext.UserId); + UserProfile userProfile = await _profileClient.GetUserProfile(userContext.UserId); userContext.UserParty = userProfile.Party; if (context.Request.Cookies[_settings.GetAltinnPartyCookieName] != null) @@ -77,7 +78,7 @@ public async Task GetUserContext(HttpContext context) } else { - userContext.Party = await _registerService.GetParty(userContext.PartyId); + userContext.Party = await _altinnPartyClientService.GetParty(userContext.PartyId); } return userContext; diff --git a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs index 2ae22081c..980d1ebfb 100644 --- a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs +++ b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs @@ -2,7 +2,6 @@ using System.Text.Json; using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Layout; @@ -120,7 +119,7 @@ public Application GetApplication() Show = applicationMetadata.OnEntry.Show }; } - + return application; } catch (AggregateException ex) @@ -464,5 +463,21 @@ private byte[] ReadFileContentsFromLegalPath(string legalPath, string filePath) return filedata; } + + /// + public string? GetValidationConfiguration(string modelId) + { + string legalPath = $"{_settings.AppBasePath}{_settings.ModelsFolder}"; + string filename = $"{legalPath}{modelId}.{_settings.ValidationConfigurationFileName}"; + PathHelper.EnsureLegalPath(legalPath, filename); + + string? filedata = null; + if (File.Exists(filename)) + { + filedata = File.ReadAllText(filename, Encoding.UTF8); + } + + return filedata; + } } } diff --git a/src/Altinn.App.Core/Implementation/DefaultAppEvents.cs b/src/Altinn.App.Core/Implementation/DefaultAppEvents.cs index 0c13c5f26..4406e7d7d 100644 --- a/src/Altinn.App.Core/Implementation/DefaultAppEvents.cs +++ b/src/Altinn.App.Core/Implementation/DefaultAppEvents.cs @@ -1,5 +1,6 @@ -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; @@ -13,8 +14,8 @@ public class DefaultAppEvents: IAppEvents { private readonly ILogger _logger; private readonly IAppMetadata _appMetadata; - private readonly IInstance _instanceClient; - private readonly IData _dataClient; + private readonly IInstanceClient _instanceClient; + private readonly IDataClient _dataClient; /// /// Constructor with services from DI @@ -22,8 +23,8 @@ public class DefaultAppEvents: IAppEvents public DefaultAppEvents( ILogger logger, IAppMetadata appMetadata, - IInstance instanceClient, - IData dataClient) + IInstanceClient instanceClient, + IDataClient dataClient) { _logger = logger; _appMetadata = appMetadata; diff --git a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs index 8218233e7..5e09d3e4c 100644 --- a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs +++ b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs @@ -5,12 +5,16 @@ using Altinn.App.Core.EFormidling.Interface; using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Pdf; +using Altinn.App.Core.Internal.Prefill; +using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -26,11 +30,11 @@ public class DefaultTaskEvents : ITaskEvents private readonly ILogger _logger; private readonly IAppResources _appResources; private readonly IAppMetadata _appMetadata; - private readonly IData _dataClient; + private readonly IDataClient _dataClient; private readonly IPrefill _prefillService; private readonly IAppModel _appModel; private readonly IInstantiationProcessor _instantiationProcessor; - private readonly IInstance _instanceClient; + private readonly IInstanceClient _instanceClient; private readonly IEnumerable _taskStarts; private readonly IEnumerable _taskEnds; private readonly IEnumerable _taskAbandons; @@ -47,11 +51,11 @@ public DefaultTaskEvents( ILogger logger, IAppResources appResources, IAppMetadata appMetadata, - IData dataClient, + IDataClient dataClient, IPrefill prefillService, IAppModel appModel, IInstantiationProcessor instantiationProcessor, - IInstance instanceClient, + IInstanceClient instanceClient, IEnumerable taskStarts, IEnumerable taskEnds, IEnumerable taskAbandons, @@ -60,7 +64,7 @@ public DefaultTaskEvents( LayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, IOptions? appSettings = null, IEFormidlingService? eFormidlingService = null - ) + ) { _logger = logger; _appResources = appResources; @@ -95,9 +99,8 @@ public async Task OnStartProcessTask(string taskId, Instance instance, Dictionar if (dataElement != null && dataElement.Locked) { - dataElement.Locked = false; _logger.LogDebug("Unlocking data element {DataElementId} of dataType {DataTypeId}", dataElement.Id, dataType.Id); - await _dataClient.Update(instance, dataElement); + await _dataClient.UnlockDataElement(new InstanceIdentifier(instance), Guid.Parse(dataElement.Id)); } } @@ -145,6 +148,8 @@ public async Task OnEndProcessTask(string endEvent, Instance instance) ApplicationMetadata appMetadata = await _appMetadata.GetApplicationMetadata(); List dataTypesToLock = appMetadata.DataTypes.FindAll(dt => dt.TaskId == endEvent); + await RunRemoveDataElementsGeneratedFromTask(instance, endEvent); + await RunRemoveHiddenData(instance, instanceGuid, dataTypesToLock); await RunRemoveShadowFields(instance, instanceGuid, dataTypesToLock); @@ -160,7 +165,7 @@ public async Task OnEndProcessTask(string endEvent, Instance instance) private async Task RunRemoveHiddenData(Instance instance, Guid instanceGuid, List? dataTypesToLock) { - if (_appSettings?.RemoveHiddenDataPreview == true) + if (_appSettings?.RemoveHiddenData == true) { await RemoveHiddenData(instance, instanceGuid, dataTypesToLock); } @@ -174,6 +179,16 @@ private async Task RunRemoveShadowFields(Instance instance, Guid instanceGuid, L } } + private async Task RunRemoveDataElementsGeneratedFromTask(Instance instance, string endEvent) + { + AppIdentifier appIdentifier = new AppIdentifier(instance.AppId); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instance); + foreach (var dataElement in instance.Data?.Where(de => de.References != null && de.References.Exists(r => r.ValueType == ReferenceType.Task && r.Value == endEvent)) ?? Enumerable.Empty()) + { + await _dataClient.DeleteData(appIdentifier.Org, appIdentifier.App, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, Guid.Parse(dataElement.Id), false); + } + } + private async Task RunAppDefinedOnTaskEnd(string endEvent, Instance instance) { foreach (var taskEnd in _taskEnds) @@ -192,16 +207,15 @@ private async Task RunLockDataAndGeneratePdf(string endEvent, Instance instance, foreach (DataElement dataElement in instance.Data.FindAll(de => de.DataType == dataType.Id)) { - dataElement.Locked = true; _logger.LogDebug("Locking data element {dataElementId} of dataType {dataTypeId}.", dataElement.Id, dataType.Id); - Task updateData = _dataClient.Update(instance, dataElement); + Task updateData = _dataClient.LockDataElement(new InstanceIdentifier(instance), Guid.Parse(dataElement.Id)); if (generatePdf) { Task createPdf; if (await _featureManager.IsEnabledAsync(FeatureFlags.NewPdfGeneration)) { - createPdf = _pdfService.GenerateAndStorePdf(instance, CancellationToken.None); + createPdf = _pdfService.GenerateAndStorePdf(instance, endEvent, CancellationToken.None); } else { @@ -255,7 +269,7 @@ private async Task RemoveHiddenData(Instance instance, Guid instanceGuid, List - public class PersonService : IPersonLookup - { - private readonly IPersonRetriever _personRetriever; - - /// - /// Initializes a new instance of the class. - /// - /// An implementation of able to obtain a . - public PersonService(IPersonRetriever personRetriever) - { - _personRetriever = personRetriever; - } - - /// - public Task GetPerson(string nationalIdentityNumber, string lastName, CancellationToken ct) - { - return _personRetriever.GetPerson(nationalIdentityNumber, lastName, ct); - } - } -} diff --git a/src/Altinn.App.Core/Implementation/PrefillSI.cs b/src/Altinn.App.Core/Implementation/PrefillSI.cs index 086a81e8a..b604a1b59 100644 --- a/src/Altinn.App.Core/Implementation/PrefillSI.cs +++ b/src/Altinn.App.Core/Implementation/PrefillSI.cs @@ -1,6 +1,9 @@ using System.Reflection; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Prefill; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; @@ -15,9 +18,9 @@ namespace Altinn.App.Core.Implementation public class PrefillSI : IPrefill { private readonly ILogger _logger; - private readonly IProfile _profileClient; + private readonly IProfileClient _profileClient; private readonly IAppResources _appResourcesService; - private readonly IRegister _registerClient; + private readonly IAltinnPartyClient _altinnPartyClientClient; private readonly IHttpContextAccessor _httpContextAccessor; private static readonly string ER_KEY = "ER"; private static readonly string DSF_KEY = "DSF"; @@ -31,19 +34,19 @@ public class PrefillSI : IPrefill /// The logger /// The profile client /// The app's resource service - /// The register client + /// The register client /// A service with access to the http context. public PrefillSI( ILogger logger, - IProfile profileClient, + IProfileClient profileClient, IAppResources appResourcesService, - IRegister registerClient, + IAltinnPartyClient altinnPartyClientClient, IHttpContextAccessor httpContextAccessor) { _logger = logger; _profileClient = profileClient; _appResourcesService = appResourcesService; - _registerClient = registerClient; + _altinnPartyClientClient = altinnPartyClientClient; _httpContextAccessor = httpContextAccessor; } @@ -75,7 +78,7 @@ public async Task PrefillDataModel(string partyId, string dataModelName, object allowOverwrite = allowOverwriteToken.ToObject(); } - Party party = await _registerClient.GetParty(int.Parse(partyId)); + Party party = await _altinnPartyClientClient.GetParty(int.Parse(partyId)); if (party == null) { string errorMessage = $"Could find party for partyId: {partyId}"; diff --git a/src/Altinn.App.Core/Implementation/UserTokenProvider.cs b/src/Altinn.App.Core/Implementation/UserTokenProvider.cs index eca3d4a1f..24dd81dae 100644 --- a/src/Altinn.App.Core/Implementation/UserTokenProvider.cs +++ b/src/Altinn.App.Core/Implementation/UserTokenProvider.cs @@ -2,7 +2,7 @@ using System.Diagnostics.CodeAnalysis; using Altinn.App.Core.Configuration; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Auth; using AltinnCore.Authentication.Utils; using Microsoft.AspNetCore.Http; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs index caf3f83f9..52938a3c5 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs @@ -2,7 +2,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Auth; using AltinnCore.Authentication.Utils; using Microsoft.AspNetCore.Http; @@ -14,7 +14,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Authentication /// /// A client for authentication actions in Altinn Platform. /// - public class AuthenticationClient : IAuthentication + public class AuthenticationClient : IAuthenticationClient { private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs index 03d7a37a9..dd3906a51 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs @@ -1,16 +1,20 @@ using System.Net.Http.Headers; +using System.Security.Claims; using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Models; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Helpers; +using Altinn.Common.PEP.Interfaces; using Altinn.Platform.Register.Models; - +using Altinn.Platform.Storage.Interface.Models; using AltinnCore.Authentication.Utils; - using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; - using Newtonsoft.Json; namespace Altinn.App.Core.Infrastructure.Clients.Authorization @@ -18,11 +22,12 @@ namespace Altinn.App.Core.Infrastructure.Clients.Authorization /// /// Client for handling authorization actions in Altinn Platform. /// - public class AuthorizationClient : IAuthorization + public class AuthorizationClient : IAuthorizationClient { private readonly IHttpContextAccessor _httpContextAccessor; private readonly AppSettings _settings; private readonly HttpClient _client; + private readonly IPDP _pdp; private readonly ILogger _logger; /// @@ -32,16 +37,19 @@ public class AuthorizationClient : IAuthorization /// the http context accessor. /// A Http client from the HttpClientFactory. /// The application settings. + /// /// the handler for logger service public AuthorizationClient( IOptions platformSettings, IHttpContextAccessor httpContextAccessor, HttpClient httpClient, IOptionsMonitor settings, + IPDP pdp, ILogger logger) { _httpContextAccessor = httpContextAccessor; _settings = settings.CurrentValue; + _pdp = pdp; _logger = logger; httpClient.BaseAddress = new Uri(platformSettings.Value.ApiAuthorizationEndpoint); httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); @@ -95,5 +103,40 @@ public AuthorizationClient( return result; } + + /// + public async Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, ClaimsPrincipal user, string action, string? taskId = null) + { + XacmlJsonRequestRoot request = DecisionHelper.CreateDecisionRequest(appIdentifier.Org, appIdentifier.App, user, action, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, taskId); + XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); + if (response?.Response == null) + { + _logger.LogWarning("Failed to get decision from pdp: {SerializeObject}", JsonConvert.SerializeObject(request)); + return false; + } + + bool authorized = DecisionHelper.ValidatePdpDecision(response.Response, user); + return authorized; + } + + /// + public async Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions) + { + XacmlJsonRequestRoot request = MultiDecisionHelper.CreateMultiDecisionRequest(user, instance, actions); + XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); + if (response?.Response == null) + { + _logger.LogWarning("Failed to get decision from pdp: {SerializeObject}", JsonConvert.SerializeObject(request)); + return new Dictionary(); + } + Dictionary actionsResult = new Dictionary(); + foreach (var action in actions) + { + actionsResult.Add(action, false); + } + return MultiDecisionHelper.ValidatePdpMultiDecision(actionsResult, response.Response, user); + } + + } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs index fbb9ced79..f67585a14 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs @@ -5,8 +5,8 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Models; using Altinn.Common.AccessTokenClient.Services; using Altinn.Platform.Storage.Interface.Models; @@ -19,9 +19,8 @@ namespace Altinn.App.Core.Infrastructure.Clients.Events /// /// A client for handling actions on events in Altinn Platform. /// - public class EventsClient : IEvents + public class EventsClient : IEventsClient { - private readonly PlatformSettings _platformSettings; private readonly IHttpContextAccessor _httpContextAccessor; private readonly AppSettings _settings; private readonly GeneralSettings _generalSettings; @@ -48,7 +47,6 @@ public EventsClient( IOptionsMonitor settings, IOptions generalSettings) { - _platformSettings = platformSettings.Value; _httpContextAccessor = httpContextAccessor; _settings = settings.CurrentValue; _generalSettings = generalSettings.Value; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsClient.cs index 755bae58d..e18d37d78 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsClient.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Secrets; using AltinnCore.Authentication.Constants; using Microsoft.Azure.KeyVault; @@ -12,7 +12,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.KeyVault /// /// Class that handles integration with Azure Key Vault /// - public class SecretsClient : ISecrets + public class SecretsClient : ISecretsClient { private readonly string _vaultUri; private readonly AzureServiceTokenProvider _azureServiceTokenProvider; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs index 074dc2691..7d5b5d0ac 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs @@ -1,16 +1,15 @@ using System.Text.Json; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Secrets; using Microsoft.Azure.KeyVault; using Microsoft.Azure.KeyVault.WebKey; using Microsoft.Extensions.Configuration; -using Newtonsoft.Json.Linq; namespace Altinn.App.Core.Infrastructure.Clients.KeyVault { /// /// Class that handles integration with Azure Key Vault /// - public class SecretsLocalClient : ISecrets + public class SecretsLocalClient : ISecretsClient { private readonly IConfiguration _configuration; @@ -24,29 +23,19 @@ public SecretsLocalClient(IConfiguration configuration) } /// - public async Task GetCertificateAsync(string certificateId) + public Task GetCertificateAsync(string certificateName) { - string token = GetTokenFromSecrets(certificateId); - if (!string.IsNullOrEmpty(token)) - { - byte[] localCertBytes = Convert.FromBase64String(token); - return await Task.FromResult(localCertBytes); - } - - return null; + string token = GetTokenFromSecrets(certificateName); + byte[] localCertBytes = Convert.FromBase64String(token); + return Task.FromResult(localCertBytes); } /// - public async Task GetKeyAsync(string keyId) + public Task GetKeyAsync(string keyName) { - string token = GetTokenFromSecrets(keyId); - if (!string.IsNullOrEmpty(token)) - { - JsonWebKey key = JsonSerializer.Deserialize(token); - return await Task.FromResult(key); - } - - return null; + string token = GetTokenFromSecrets(keyName); + JsonWebKey key = JsonSerializer.Deserialize(token)!; + return Task.FromResult(key); } /// @@ -62,25 +51,28 @@ public async Task GetSecretAsync(string secretId) return await Task.FromResult(token); } - private string GetTokenFromSecrets(string tokenId) - => GetTokenFromConfiguration(tokenId) ?? - GetTokenFromLocalSecrets(tokenId); + private string GetTokenFromSecrets(string secretId) + => GetTokenFromLocalSecrets(secretId) ?? + GetTokenFromConfiguration(secretId) ?? + throw new ArgumentException($"SecretId={secretId} does not exist in appsettings or secrets.json"); - private string GetTokenFromConfiguration(string tokenId) + private string? GetTokenFromConfiguration(string tokenId) => _configuration[tokenId]; - private static string GetTokenFromLocalSecrets(string tokenId) + private static string? GetTokenFromLocalSecrets(string secretId) { string path = Path.Combine(Directory.GetCurrentDirectory(), @"secrets.json"); if (File.Exists(path)) { string jsonString = File.ReadAllText(path); - JObject keyVault = JObject.Parse(jsonString); - keyVault.TryGetValue(tokenId, out JToken? token); - return token != null ? token.ToString() : string.Empty; + var document = JsonDocument.Parse(jsonString, new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }); + if (document.RootElement.TryGetProperty(secretId, out var jsonElement)) + { + return jsonElement.GetString(); + } } - return string.Empty; + return null; } } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs index c650f548d..1033ebade 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs @@ -3,11 +3,10 @@ using Altinn.App.Core.Internal.Pdf; -using Altinn.App.Core.Interface; - using Microsoft.Extensions.Options; using Altinn.App.Core.Models.Pdf; using Altinn.App.Core.Configuration; +using Altinn.App.Core.Internal.Auth; namespace Altinn.App.Core.Infrastructure.Clients.Pdf; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs index c667b6112..65e081f14 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs @@ -2,8 +2,9 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Models; using Altinn.Common.AccessTokenClient.Services; using Altinn.Platform.Profile.Models; @@ -17,7 +18,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Profile /// /// A client for retrieving profiles from Altinn Platform. /// - public class ProfileClient : IProfile + public class ProfileClient : IProfileClient { private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; @@ -57,9 +58,9 @@ public ProfileClient( } /// - public async Task GetUserProfile(int userId) + public async Task GetUserProfile(int userId) { - UserProfile userProfile = null; + UserProfile? userProfile = null; string endpointUrl = $"users/{userId}"; string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); @@ -68,7 +69,7 @@ public async Task GetUserProfile(int userId) HttpResponseMessage response = await _client.GetAsync(token, endpointUrl, _accessTokenGenerator.GenerateAccessToken(applicationMetadata.Org, applicationMetadata.AppIdentifier.App)); if (response.StatusCode == System.Net.HttpStatusCode.OK) { - userProfile = await response.Content.ReadAsAsync(); + userProfile = await JsonSerializerPermissive.DeserializeAsync(response.Content); } else { diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs index 199952b3a..68662b4c2 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs @@ -1,5 +1,5 @@ using Altinn.App.Core.Configuration; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Profile; using Altinn.Platform.Profile.Models; using Microsoft.Extensions.Caching.Memory; @@ -8,19 +8,19 @@ namespace Altinn.App.Core.Infrastructure.Clients.Profile { /// . - /// Decorates an implementation of IProfile by caching the party object. + /// Decorates an implementation of IProfileClient by caching the party object. /// If available, object is retrieved from cache without calling the service /// - public class ProfileClientCachingDecorator : IProfile + public class ProfileClientCachingDecorator : IProfileClient { - private readonly IProfile _decoratedService; + private readonly IProfileClient _decoratedService; private readonly IMemoryCache _memoryCache; private readonly MemoryCacheEntryOptions _cacheOptions; /// /// Initializes a new instance of the class. /// - public ProfileClientCachingDecorator(IProfile decoratedService, IMemoryCache memoryCache, IOptions _settings) + public ProfileClientCachingDecorator(IProfileClient decoratedService, IMemoryCache memoryCache, IOptions _settings) { _decoratedService = decoratedService; _memoryCache = memoryCache; @@ -32,11 +32,11 @@ public ProfileClientCachingDecorator(IProfile decoratedService, IMemoryCache mem } /// - public async Task GetUserProfile(int userId) + public async Task GetUserProfile(int userId) { string uniqueCacheKey = "User_UserId_" + userId; - if (_memoryCache.TryGetValue(uniqueCacheKey, out UserProfile user)) + if (_memoryCache.TryGetValue(uniqueCacheKey, out UserProfile? user)) { return user; } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs similarity index 80% rename from src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterClient.cs rename to src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs index becbd326e..22542922f 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs @@ -4,8 +4,8 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Altinn.Common.AccessTokenClient.Services; using Altinn.Platform.Register.Models; @@ -13,17 +13,14 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Newtonsoft.Json; namespace Altinn.App.Core.Infrastructure.Clients.Register { /// /// A client for retrieving register data from Altinn Platform. /// - public class RegisterClient : IRegister + public class AltinnPartyClient : IAltinnPartyClient { - private readonly IDSF _dsfClient; - private readonly IER _erClient; private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; private readonly AppSettings _settings; @@ -32,30 +29,24 @@ public class RegisterClient : IRegister private readonly IAccessTokenGenerator _accessTokenGenerator; /// - /// Initializes a new instance of the class + /// Initializes a new instance of the class /// /// The current platform settings. - /// The dsf - /// The er /// The logger /// The http context accessor /// The application settings. /// The http client /// The app metadata service /// The platform access token generator - public RegisterClient( + public AltinnPartyClient( IOptions platformSettings, - IDSF dsf, - IER er, - ILogger logger, + ILogger logger, IHttpContextAccessor httpContextAccessor, IOptionsMonitor settings, HttpClient httpClient, IAppMetadata appMetadata, IAccessTokenGenerator accessTokenGenerator) { - _dsfClient = dsf; - _erClient = er; _logger = logger; _httpContextAccessor = httpContextAccessor; _settings = settings.CurrentValue; @@ -67,24 +58,8 @@ public RegisterClient( _accessTokenGenerator = accessTokenGenerator; } - /// - /// The access to the dsf component through register services - /// - public IDSF DSF - { - get { return _dsfClient; } - } - - /// - /// The access to the er component through register services - /// - public IER ER - { - get { return _erClient; } - } - /// - public async Task GetParty(int partyId) + public async Task GetParty(int partyId) { Party? party = null; @@ -94,7 +69,7 @@ public async Task GetParty(int partyId) HttpResponseMessage response = await _client.GetAsync(token, endpointUrl, _accessTokenGenerator.GenerateAccessToken(application.Org, application.AppIdentifier.App)); if (response.StatusCode == HttpStatusCode.OK) { - party = await response.Content.ReadAsAsync(); + party = await JsonSerializerPermissive.DeserializeAsync(response.Content); } else if (response.StatusCode == HttpStatusCode.Unauthorized) { @@ -116,7 +91,7 @@ public async Task LookupParty(PartyLookup partyLookup) string endpointUrl = "parties/lookup"; string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); - StringContent content = new StringContent(JsonConvert.SerializeObject(partyLookup)); + StringContent content = new StringContent(JsonSerializerPermissive.Serialize(partyLookup)); content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); HttpRequestMessage request = new HttpRequestMessage { @@ -132,7 +107,7 @@ public async Task LookupParty(PartyLookup partyLookup) HttpResponseMessage response = await _client.SendAsync(request); if (response.StatusCode == HttpStatusCode.OK) { - party = await response.Content.ReadAsAsync(); + party = await JsonSerializerPermissive.DeserializeAsync(response.Content); } else { diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs index 63bed1f14..bd8317546 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs @@ -8,8 +8,9 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Altinn.Common.AccessTokenClient.Services; using Altinn.Platform.Register.Models; @@ -18,10 +19,10 @@ namespace Altinn.App.Core.Infrastructure.Clients.Register { /// - /// Represents an implementation of that will call the Register + /// Represents an implementation of that will call the Register /// component to retrieve person information. /// - public class PersonClient : IPersonRetriever + public class PersonClient : IPersonClient { private readonly HttpClient _httpClient; private readonly IAppMetadata _appMetadata; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterDSFClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterDSFClient.cs deleted file mode 100644 index f715969e9..000000000 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterDSFClient.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Net.Http.Headers; -using Altinn.App.Core.Configuration; -using Altinn.App.Core.Constants; -using Altinn.App.Core.Extensions; -using Altinn.App.Core.Interface; -using Altinn.App.Core.Internal.App; -using Altinn.App.Core.Models; -using Altinn.Common.AccessTokenClient.Services; -using Altinn.Platform.Register.Models; -using AltinnCore.Authentication.Utils; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Altinn.App.Core.Infrastructure.Clients.Register -{ - /// - /// A client for retriecing DSF data from Altinn Platform. - /// - public class RegisterDSFClient : IDSF - { - private readonly ILogger _logger; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly AppSettings _settings; - private readonly HttpClient _client; - private readonly IAccessTokenGenerator _accessTokenGenerator; - private readonly IAppMetadata _appMetadata; - - /// - /// Initializes a new instance of the class - /// - /// The platform settings from loaded configuration. - /// the logger - /// The http context accessor - /// The application settings. - /// The http client - /// The platform access token generator - /// The app metadata service - public RegisterDSFClient( - IOptions platformSettings, - ILogger logger, - IHttpContextAccessor httpContextAccessor, - IOptionsMonitor settings, - HttpClient httpClient, - IAccessTokenGenerator accessTokenGenerator, - IAppMetadata appMetadata) - { - _logger = logger; - _httpContextAccessor = httpContextAccessor; - _settings = settings.CurrentValue; - httpClient.BaseAddress = new Uri(platformSettings.Value.ApiRegisterEndpoint); - httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - _client = httpClient; - _accessTokenGenerator = accessTokenGenerator; - _appMetadata = appMetadata; - } - - /// - public async Task GetPerson(string SSN) - { - Person? person = null; - - string endpointUrl = $"persons/{SSN}"; - - string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); - - ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); - HttpResponseMessage response = await _client.GetAsync(token, endpointUrl, _accessTokenGenerator.GenerateAccessToken(application.Org, application.AppIdentifier.App)); - if (response.StatusCode == System.Net.HttpStatusCode.OK) - { - person = await response.Content.ReadAsAsync(); - } - else - { - _logger.LogError("Getting person with ssn {Ssn} failed with statuscode {StatusCode}", SSN, response.StatusCode); - } - - return person; - } - } -} diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs index 6a6e9c0fd..6e1ca9048 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs @@ -2,8 +2,9 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Altinn.Common.AccessTokenClient.Services; using Altinn.Platform.Register.Models; @@ -17,7 +18,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Register /// /// A client for retrieving ER data from Altinn Platform. /// - public class RegisterERClient : IER + public class RegisterERClient : IOrganizationClient { private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; @@ -69,7 +70,7 @@ public RegisterERClient( if (response.StatusCode == System.Net.HttpStatusCode.OK) { - organization = await response.Content.ReadAsAsync(); + organization = await JsonSerializerPermissive.DeserializeAsync(response.Content); } else { diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ApplicationClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ApplicationClient.cs index 173c7fd12..e1c8dd9c1 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ApplicationClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ApplicationClient.cs @@ -2,7 +2,7 @@ using System.Net.Http.Headers; using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; @@ -15,7 +15,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Storage /// /// Client for retrieving application for Altinn Platform /// - public class ApplicationClient : IApplication + public class ApplicationClient : IApplicationClient { private readonly ILogger _logger; private readonly HttpClient _client; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index 717c72701..d72dd04bc 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -8,27 +8,24 @@ using Altinn.App.Core.Helpers.Serialization; using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; -using AltinnCore.Authentication.Utils; - using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; using Newtonsoft.Json; using System.Xml; -using Microsoft.IdentityModel.Tokens; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Data; namespace Altinn.App.Core.Infrastructure.Clients.Storage { /// /// A client for handling actions on data in Altinn Platform. /// - public class DataClient : IData + public class DataClient : IDataClient { private readonly PlatformSettings _platformSettings; private readonly ILogger _logger; @@ -207,7 +204,7 @@ public async Task> GetBinaryDataList(string org, string app return attachmentList; } - _logger.Log(LogLevel.Error, "Unable to fetch attachment list {0}", response.StatusCode); + _logger.Log(LogLevel.Error, "Unable to fetch attachment list {statusCode}", response.StatusCode); throw await PlatformHttpException.CreateAsync(response); } @@ -215,7 +212,7 @@ public async Task> GetBinaryDataList(string org, string app private static void ExtractAttachments(List dataList, List attachmentList) { List? attachments = null; - IEnumerable attachmentTypes = dataList.GroupBy(m => m.DataType).Select(m => m.FirstOrDefault()); + IEnumerable attachmentTypes = dataList.GroupBy(m => m.DataType).Select(m => m.First()); foreach (DataElement attachmentType in attachmentTypes) { @@ -294,9 +291,13 @@ public async Task InsertBinaryData(string org, string app, int inst } /// - public async Task InsertBinaryData(string instanceId, string dataType, string contentType, string filename, Stream stream) + public async Task InsertBinaryData(string instanceId, string dataType, string contentType, string? filename, Stream stream, string? generatedFromTask = null) { string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceId}/data?dataType={dataType}"; + if(!string.IsNullOrEmpty(generatedFromTask)) + { + apiUrl += $"&generatedFromTask={generatedFromTask}"; + } string token = _userTokenProvider.GetUserToken(); DataElement dataElement; @@ -390,5 +391,37 @@ public async Task Update(Instance instance, DataElement dataElement throw await PlatformHttpException.CreateAsync(response); } + + /// + public async Task LockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + { + string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock"; + string token = _userTokenProvider.GetUserToken(); + _logger.LogDebug("Locking data element {DataGuid} for instance {InstanceIdentifier} URL: {Url}", dataGuid, instanceIdentifier, apiUrl); + HttpResponseMessage response = await _client.PutAsync(token, apiUrl, content: null); + if (response.IsSuccessStatusCode) + { + DataElement result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync())!; + return result; + } + _logger.LogError("Locking data element {DataGuid} for instance {InstanceIdentifier} failed with status code {StatusCode}", dataGuid, instanceIdentifier, response.StatusCode); + throw await PlatformHttpException.CreateAsync(response); + } + + /// + public async Task UnlockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + { + string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock"; + string token = _userTokenProvider.GetUserToken(); + _logger.LogDebug("Unlocking data element {DataGuid} for instance {InstanceIdentifier} URL: {Url}", dataGuid, instanceIdentifier, apiUrl); + HttpResponseMessage response = await _client.DeleteAsync(token, apiUrl); + if (response.IsSuccessStatusCode) + { + DataElement result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync())!; + return result; + } + _logger.LogError("Unlocking data element {DataGuid} for instance {InstanceIdentifier} failed with status code {StatusCode}", dataGuid, instanceIdentifier, response.StatusCode); + throw await PlatformHttpException.CreateAsync(response); + } } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs index 3844531e3..9c57d0ccf 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs @@ -5,7 +5,7 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; @@ -23,7 +23,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Storage /// /// A client for handling actions on instances in Altinn Platform. /// - public class InstanceClient : IInstance + public class InstanceClient : IInstanceClient { private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClientMetricsDecorator.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClientMetricsDecorator.cs new file mode 100644 index 000000000..a9d85ec54 --- /dev/null +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClientMetricsDecorator.cs @@ -0,0 +1,123 @@ +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Internal.Instances; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Primitives; +using Prometheus; + +namespace Altinn.App.Core.Infrastructure.Clients.Storage; + +/// +/// Decorator for the instance client that adds metrics for the number of instances created, completed and deleted. +/// +public class InstanceClientMetricsDecorator : IInstanceClient +{ + private readonly IInstanceClient _instanceClient; + private static readonly Counter InstancesCreatedCounter = Metrics.CreateCounter("altinn_app_instances_created", "Number of instances created", "result"); + private static readonly Counter InstancesCompletedCounter = Metrics.CreateCounter("altinn_app_instances_completed", "Number of instances completed", "result"); + private static readonly Counter InstancesDeletedCounter = Metrics.CreateCounter("altinn_app_instances_deleted", "Number of instances completed", "result", "mode" ); + + /// + /// Create a new instance of the class. + /// + /// The instance client to decorate. + public InstanceClientMetricsDecorator(IInstanceClient instanceClient) + { + _instanceClient = instanceClient; + } + + /// + public async Task GetInstance(string app, string org, int instanceOwnerPartyId, Guid instanceId) + { + return await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceId); + } + + /// + public async Task GetInstance(Instance instance) + { + return await _instanceClient.GetInstance(instance); + } + + /// + public async Task> GetInstances(Dictionary queryParams) + { + return await _instanceClient.GetInstances(queryParams); + } + + /// + public async Task UpdateProcess(Instance instance) + { + return await _instanceClient.UpdateProcess(instance); + } + + /// + public async Task CreateInstance(string org, string app, Instance instanceTemplate) + { + var success = false; + try + { + var instance = await _instanceClient.CreateInstance(org, app, instanceTemplate); + success = true; + return instance; + } + finally + { + InstancesCreatedCounter.WithLabels(success ? "success" : "failure").Inc(); + } + } + + /// + public async Task AddCompleteConfirmation(int instanceOwnerPartyId, Guid instanceGuid) + { + var success = false; + try + { + var instance = await _instanceClient.AddCompleteConfirmation(instanceOwnerPartyId, instanceGuid); + success = true; + return instance; + } + finally + { + InstancesCompletedCounter.WithLabels(success ? "success" : "failure").Inc(); + } + } + + /// + public async Task UpdateReadStatus(int instanceOwnerPartyId, Guid instanceGuid, string readStatus) + { + return await _instanceClient.UpdateReadStatus(instanceOwnerPartyId, instanceGuid, readStatus); + } + + /// + public async Task UpdateSubstatus(int instanceOwnerPartyId, Guid instanceGuid, Substatus substatus) + { + return await _instanceClient.UpdateSubstatus(instanceOwnerPartyId, instanceGuid, substatus); + } + + /// + public async Task UpdatePresentationTexts(int instanceOwnerPartyId, Guid instanceGuid, PresentationTexts presentationTexts) + { + return await _instanceClient.UpdatePresentationTexts(instanceOwnerPartyId, instanceGuid, presentationTexts); + } + + /// + public async Task UpdateDataValues(int instanceOwnerPartyId, Guid instanceGuid, DataValues dataValues) + { + return await _instanceClient.UpdateDataValues(instanceOwnerPartyId, instanceGuid, dataValues); + } + + /// + public async Task DeleteInstance(int instanceOwnerPartyId, Guid instanceGuid, bool hard) + { + var success = false; + try + { + var deleteInstance = await _instanceClient.DeleteInstance(instanceOwnerPartyId, instanceGuid, hard); + success = true; + return deleteInstance; + } + finally + { + InstancesDeletedCounter.WithLabels(success ? "success" : "failure", hard ? "hard" : "soft").Inc(); + } + } +} diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs index b13f07e88..8d0a243db 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs @@ -4,7 +4,7 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Instances; using Altinn.Platform.Storage.Interface.Models; using AltinnCore.Authentication.Utils; @@ -18,7 +18,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Storage /// /// A client for handling actions on instance events in Altinn Platform. /// - public class InstanceEventClient : IInstanceEvent + public class InstanceEventClient : IInstanceEventClient { private readonly IHttpContextAccessor _httpContextAccessor; private readonly AppSettings _settings; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs index 7d3486019..97424abe7 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs @@ -3,7 +3,7 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Process; using Altinn.Platform.Storage.Interface.Models; using AltinnCore.Authentication.Utils; using Microsoft.AspNetCore.Http; @@ -16,11 +16,10 @@ namespace Altinn.App.Core.Infrastructure.Clients.Storage /// /// The app implementation of the process service. /// - public class ProcessClient : IProcess + public class ProcessClient : IProcessClient { private readonly AppSettings _appSettings; private readonly ILogger _logger; - private readonly IInstanceEvent _instanceEventClient; private readonly HttpClient _client; private readonly IHttpContextAccessor _httpContextAccessor; @@ -30,13 +29,11 @@ public class ProcessClient : IProcess public ProcessClient( IOptions platformSettings, IOptions appSettings, - IInstanceEvent instanceEventClient, ILogger logger, IHttpContextAccessor httpContextAccessor, HttpClient httpClient) { _appSettings = appSettings.Value; - _instanceEventClient = instanceEventClient; _httpContextAccessor = httpContextAccessor; _logger = logger; httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint); @@ -49,7 +46,7 @@ public ProcessClient( /// public Stream GetProcessDefinition() { - string bpmnFilePath = _appSettings.AppBasePath + _appSettings.ConfigurationFolder + _appSettings.ProcessFolder + _appSettings.ProcessFileName; + string bpmnFilePath = Path.Join(_appSettings.AppBasePath , _appSettings.ConfigurationFolder , _appSettings.ProcessFolder , _appSettings.ProcessFileName); try { @@ -82,18 +79,5 @@ public async Task GetProcessHistory(string instanceGuid, str throw await PlatformHttpException.CreateAsync(response); } - - /// - public async Task DispatchProcessEventsToStorage(Instance instance, List events) - { - string org = instance.Org; - string app = instance.AppId.Split("/")[1]; - - foreach (InstanceEvent instanceEvent in events) - { - instanceEvent.InstanceId = instance.Id; - await _instanceEventClient.SaveInstanceEvent(instanceEvent, org, app); - } - } } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs new file mode 100644 index 000000000..227a04833 --- /dev/null +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs @@ -0,0 +1,82 @@ +using System.Net.Http.Headers; +using System.Net.Http.Json; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Constants; +using Altinn.App.Core.Extensions; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Sign; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Core.Infrastructure.Clients.Storage +{ + /// + /// Implementation of that sends signing requests to platform + /// + public class SignClient: ISignClient + { + private readonly IUserTokenProvider _userTokenProvider; + private readonly HttpClient _client; + + /// + /// Create a new instance of + /// + /// Platform settings, used to get storage endpoint + /// HttpClient used to send requests + /// Service that can provide user token + public SignClient( + IOptions platformSettings, + HttpClient httpClient, + IUserTokenProvider userTokenProvider) + { + var platformSettings1 = platformSettings.Value; + + httpClient.BaseAddress = new Uri(platformSettings1.ApiStorageEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings1.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); + _client = httpClient; + _userTokenProvider = userTokenProvider; + } + + /// + public async Task SignDataElements(SignatureContext signatureContext) + { + string apiUrl = $"instances/{signatureContext.InstanceIdentifier}/sign"; + string token = _userTokenProvider.GetUserToken(); + HttpResponseMessage response = await _client.PostAsync(token, apiUrl, BuildSignRequest(signatureContext)); + if (response.IsSuccessStatusCode) + { + return; + } + + throw new PlatformHttpException(response, "Failed to sign dataelements"); + } + + private static JsonContent BuildSignRequest(SignatureContext signatureContext) + { + SignRequest signRequest = new SignRequest() + { + Signee = new() + { + UserId = signatureContext.Signee.UserId, + PersonNumber = signatureContext.Signee.PersonNumber, + OrganisationNumber = signatureContext.Signee.OrganisationNumber + }, + SignatureDocumentDataType = signatureContext.SignatureDataTypeId, + DataElementSignatures = new() + }; + foreach (var dataElementSignature in signatureContext.DataElementSignatures) + { + signRequest.DataElementSignatures.Add(new SignRequest.DataElementSignature() + { + DataElementId = dataElementSignature.DataElementId, + Signed = dataElementSignature.Signed + }); + } + + return JsonContent.Create(signRequest); + } + } +} diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs index 5653b5a75..21519e87f 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs @@ -2,6 +2,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Texts; using Altinn.Platform.Storage.Interface.Models; using AltinnCore.Authentication.Utils; @@ -16,6 +17,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Storage /// /// A client forretrieving text resources from Altinn Platform. /// + [Obsolete("Use IAppResources.GetTexts() instead")] public class TextClient : IText { private readonly ILogger _logger; @@ -58,9 +60,9 @@ public TextClient( } /// - public async Task GetText(string org, string app, string language) + public async Task GetText(string org, string app, string language) { - TextResource textResource = null; + TextResource? textResource = null; string cacheKey = $"{org}-{app}-{language.ToLower()}"; if (!_memoryCache.TryGetValue(cacheKey, out textResource)) @@ -72,7 +74,7 @@ public async Task GetText(string org, string app, string language) HttpResponseMessage response = await _client.GetAsync(token, url); if (response.StatusCode == System.Net.HttpStatusCode.OK) { - textResource = await response.Content.ReadAsAsync(); + textResource = await JsonSerializerPermissive.DeserializeAsync(response.Content); _memoryCache.Set(cacheKey, textResource, cacheEntryOptions); } else diff --git a/src/Altinn.App.Core/Interface/IAppEvents.cs b/src/Altinn.App.Core/Interface/IAppEvents.cs index 6c4d04dd5..9db8e3186 100644 --- a/src/Altinn.App.Core/Interface/IAppEvents.cs +++ b/src/Altinn.App.Core/Interface/IAppEvents.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface; /// /// Interface for implementing a receiver handling instance events. /// +[Obsolete(message: "Use Altinn.App.Core.Internal.App.IAppEvents instead", error: true)] public interface IAppEvents { /// diff --git a/src/Altinn.App.Core/Interface/IAppResources.cs b/src/Altinn.App.Core/Interface/IAppResources.cs index b4e471e69..1b2978d24 100644 --- a/src/Altinn.App.Core/Interface/IAppResources.cs +++ b/src/Altinn.App.Core/Interface/IAppResources.cs @@ -7,6 +7,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for execution functionality /// + [Obsolete(message: "Use Altinn.App.Core.Internal.App.IAppResources instead", error: true)] public interface IAppResources { /// diff --git a/src/Altinn.App.Core/Interface/IApplication.cs b/src/Altinn.App.Core/Interface/IApplication.cs index 64ae78f4e..b2caf3d88 100644 --- a/src/Altinn.App.Core/Interface/IApplication.cs +++ b/src/Altinn.App.Core/Interface/IApplication.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for retrieving application metadata data related operations /// + [Obsolete(message: "Use Altinn.App.Core.Internal.App.IApplicationClient instead", error: true)] public interface IApplication { /// diff --git a/src/Altinn.App.Core/Interface/IAuthentication.cs b/src/Altinn.App.Core/Interface/IAuthentication.cs index b81b2b707..e0c40ae5b 100644 --- a/src/Altinn.App.Core/Interface/IAuthentication.cs +++ b/src/Altinn.App.Core/Interface/IAuthentication.cs @@ -3,6 +3,7 @@ namespace Altinn.App.Core.Interface /// /// Authentication interface. /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Auth.IAuthenticationClient instead", error: true)] public interface IAuthentication { /// diff --git a/src/Altinn.App.Core/Interface/IAuthorization.cs b/src/Altinn.App.Core/Interface/IAuthorization.cs index 282f1fe02..a8ec8ef1c 100644 --- a/src/Altinn.App.Core/Interface/IAuthorization.cs +++ b/src/Altinn.App.Core/Interface/IAuthorization.cs @@ -1,3 +1,5 @@ +using System.Security.Claims; +using Altinn.App.Core.Models; using Altinn.Platform.Register.Models; namespace Altinn.App.Core.Interface @@ -5,6 +7,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for authorization functionality. /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Auth.IAuthorizationClient instead", error: true)] public interface IAuthorization { /// @@ -21,5 +24,16 @@ public interface IAuthorization /// The party id. /// Boolean indicating whether or not the user can represent the selected party. Task ValidateSelectedParty(int userId, int partyId); + + /// + /// Check if the user is authorized to perform the given action on the given instance. + /// + /// + /// + /// + /// + /// + /// + Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, ClaimsPrincipal user, string action, string? taskId = null); } } diff --git a/src/Altinn.App.Core/Interface/IDSF.cs b/src/Altinn.App.Core/Interface/IDSF.cs index 4d34dd5fb..dc8204825 100644 --- a/src/Altinn.App.Core/Interface/IDSF.cs +++ b/src/Altinn.App.Core/Interface/IDSF.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for the resident registration database (DSF: Det sentrale folkeregisteret) /// + [Obsolete(message: "Upstream API changed. Use Altinn.App.Core.Internal.Registers.IPersonClient instead", error: true)] public interface IDSF { /// diff --git a/src/Altinn.App.Core/Interface/IData.cs b/src/Altinn.App.Core/Interface/IData.cs index 643dfd037..742c5f6b2 100644 --- a/src/Altinn.App.Core/Interface/IData.cs +++ b/src/Altinn.App.Core/Interface/IData.cs @@ -7,6 +7,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for data handling /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Data.IDataClient instead", error: true)] public interface IData { /// diff --git a/src/Altinn.App.Core/Interface/IER.cs b/src/Altinn.App.Core/Interface/IER.cs index a95e0c99c..18f497dfa 100644 --- a/src/Altinn.App.Core/Interface/IER.cs +++ b/src/Altinn.App.Core/Interface/IER.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for the entity registry (ER: Enhetsregisteret) /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IOrganizationClient instead", error: true)] public interface IER { /// diff --git a/src/Altinn.App.Core/Interface/IEvents.cs b/src/Altinn.App.Core/Interface/IEvents.cs index c635e2e38..5ed3a1ccf 100644 --- a/src/Altinn.App.Core/Interface/IEvents.cs +++ b/src/Altinn.App.Core/Interface/IEvents.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface /// /// Interface describing client implementations for the Events component in the Altinn 3 platform. /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Events.IEventsClient instead", error: true)] public interface IEvents { /// diff --git a/src/Altinn.App.Core/Interface/IInstance.cs b/src/Altinn.App.Core/Interface/IInstance.cs index 875ff9a1d..46b5245b4 100644 --- a/src/Altinn.App.Core/Interface/IInstance.cs +++ b/src/Altinn.App.Core/Interface/IInstance.cs @@ -7,6 +7,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for handling form data related operations /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Instances.IInstanceClient instead", error: true)] public interface IInstance { /// diff --git a/src/Altinn.App.Core/Interface/IInstanceEvent.cs b/src/Altinn.App.Core/Interface/IInstanceEvent.cs index 60a1bebb3..9669aaab8 100644 --- a/src/Altinn.App.Core/Interface/IInstanceEvent.cs +++ b/src/Altinn.App.Core/Interface/IInstanceEvent.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for handling instance event related operations /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Instances.IInstanceEventClient instead", error: true)] public interface IInstanceEvent { /// diff --git a/src/Altinn.App.Core/Interface/IPersonLookup.cs b/src/Altinn.App.Core/Interface/IPersonLookup.cs index 982d528cd..bfbb867d1 100644 --- a/src/Altinn.App.Core/Interface/IPersonLookup.cs +++ b/src/Altinn.App.Core/Interface/IPersonLookup.cs @@ -7,6 +7,7 @@ namespace Altinn.App.Core.Interface /// /// Describes the methods required by a person lookup service. /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IPersonClient instead", error: true)] public interface IPersonLookup { /// diff --git a/src/Altinn.App.Core/Interface/IPersonRetriever.cs b/src/Altinn.App.Core/Interface/IPersonRetriever.cs index 209d603d8..cf09d6e26 100644 --- a/src/Altinn.App.Core/Interface/IPersonRetriever.cs +++ b/src/Altinn.App.Core/Interface/IPersonRetriever.cs @@ -7,6 +7,7 @@ namespace Altinn.App.Core.Interface /// /// Describes the required methods for an implementation of a person repository client. /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IPersonClient instead", error: true)] public interface IPersonRetriever { /// diff --git a/src/Altinn.App.Core/Interface/IPrefill.cs b/src/Altinn.App.Core/Interface/IPrefill.cs index af36409d5..cdd9c9153 100644 --- a/src/Altinn.App.Core/Interface/IPrefill.cs +++ b/src/Altinn.App.Core/Interface/IPrefill.cs @@ -3,6 +3,7 @@ namespace Altinn.App.Core.Interface /// /// The prefill service /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Prefill.IPrefill instead", error: true)] public interface IPrefill { /// diff --git a/src/Altinn.App.Core/Interface/IProcess.cs b/src/Altinn.App.Core/Interface/IProcess.cs index 92f98e3fa..44288ef87 100644 --- a/src/Altinn.App.Core/Interface/IProcess.cs +++ b/src/Altinn.App.Core/Interface/IProcess.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface /// /// Process service that encapsulate reading of the BPMN process definition. /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Process.IProcessClient instead", error: true)] public interface IProcess { /// @@ -13,14 +14,6 @@ public interface IProcess /// the stream Stream GetProcessDefinition(); - /// - /// Dispatches process events to storage. - /// - /// the instance - /// process events - /// - public Task DispatchProcessEventsToStorage(Instance instance, List events); - /// /// Gets the instance process events related to the instance matching the instance id. /// diff --git a/src/Altinn.App.Core/Interface/IProcessChangeHandler.cs b/src/Altinn.App.Core/Interface/IProcessChangeHandler.cs deleted file mode 100644 index 3fc247603..000000000 --- a/src/Altinn.App.Core/Interface/IProcessChangeHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Altinn.App.Core.Models; - -namespace Altinn.App.Core.Interface -{ - /// - /// Interface for Process Change Handler. Responsible for triggering events - /// - public interface IProcessChangeHandler - { - /// - /// Handle start of process - /// - Task HandleStart(ProcessChangeContext processChange); - - /// - /// Handle complete task and move to - /// - /// - Task HandleMoveToNext(ProcessChangeContext processChange); - - /// - /// Handle start task - /// - Task HandleStartTask(ProcessChangeContext processChange); - - /// - /// Check if current task can be completed - /// - /// - Task CanTaskBeEnded(ProcessChangeContext processChange); - } -} diff --git a/src/Altinn.App.Core/Interface/IProcessEngine.cs b/src/Altinn.App.Core/Interface/IProcessEngine.cs deleted file mode 100644 index ee2af25a1..000000000 --- a/src/Altinn.App.Core/Interface/IProcessEngine.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Threading.Tasks; -using Altinn.App.Core.Models; - -namespace Altinn.App.Core.Interface -{ - /// - /// Process engine interface that defines the Altinn App process engine - /// - public interface IProcessEngine - { - /// - /// Method to start a new process - /// - Task StartProcess(ProcessChangeContext processChange); - - /// - /// Method to move process to next task/event - /// - Task Next(ProcessChangeContext processChange); - - /// - /// Method to Start Task - /// - Task StartTask(ProcessChangeContext processChange); - } -} diff --git a/src/Altinn.App.Core/Interface/IProfile.cs b/src/Altinn.App.Core/Interface/IProfile.cs index 65c0e6054..2c0db8a98 100644 --- a/src/Altinn.App.Core/Interface/IProfile.cs +++ b/src/Altinn.App.Core/Interface/IProfile.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for profile functionality /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Profile.IProfileClient instead", error: true)] public interface IProfile { /// diff --git a/src/Altinn.App.Core/Interface/IRegister.cs b/src/Altinn.App.Core/Interface/IRegister.cs index 9856fb155..edfc92f97 100644 --- a/src/Altinn.App.Core/Interface/IRegister.cs +++ b/src/Altinn.App.Core/Interface/IRegister.cs @@ -5,18 +5,9 @@ namespace Altinn.App.Core.Interface /// /// Interface for register functionality /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IAltinnPartyClient instead", error: true)] public interface IRegister { - /// - /// The access to dsf methods through register - /// - IDSF DSF { get; } - - /// - /// The access to er methods through register - /// - IER ER { get; } - /// /// Returns party information /// diff --git a/src/Altinn.App.Core/Interface/ISecrets.cs b/src/Altinn.App.Core/Interface/ISecrets.cs index f717a0433..14e8a2ccd 100644 --- a/src/Altinn.App.Core/Interface/ISecrets.cs +++ b/src/Altinn.App.Core/Interface/ISecrets.cs @@ -6,6 +6,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for secrets service /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Secrets.ISecretsClient instead", error: true)] public interface ISecrets { /// diff --git a/src/Altinn.App.Core/Interface/ITaskEvents.cs b/src/Altinn.App.Core/Interface/ITaskEvents.cs index 928fb87ea..0af8bb45a 100644 --- a/src/Altinn.App.Core/Interface/ITaskEvents.cs +++ b/src/Altinn.App.Core/Interface/ITaskEvents.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface; /// /// Interface for implementing a receiver handling task process events. /// +[Obsolete(message: "Use Altinn.App.Core.Internal.Process.ITaskEvents instead", error: true)] public interface ITaskEvents { /// diff --git a/src/Altinn.App.Core/Interface/IUserTokenProvider.cs b/src/Altinn.App.Core/Interface/IUserTokenProvider.cs index ea8a8db5c..86e30ec29 100644 --- a/src/Altinn.App.Core/Interface/IUserTokenProvider.cs +++ b/src/Altinn.App.Core/Interface/IUserTokenProvider.cs @@ -7,6 +7,7 @@ namespace Altinn.App.Core.Interface /// The provider is used by client implementations that needs the user token in requests /// against other systems. /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Auth.IUserTokenProvider instead", error: true)] public interface IUserTokenProvider { /// diff --git a/src/Altinn.App.Core/Internal/App/ApplicationConfigException.cs b/src/Altinn.App.Core/Internal/App/ApplicationConfigException.cs index f89490331..e04d91078 100644 --- a/src/Altinn.App.Core/Internal/App/ApplicationConfigException.cs +++ b/src/Altinn.App.Core/Internal/App/ApplicationConfigException.cs @@ -5,7 +5,6 @@ namespace Altinn.App.Core.Internal.App /// /// Configuration is not valid for application /// - [Serializable] public class ApplicationConfigException: Exception { @@ -16,15 +15,6 @@ public ApplicationConfigException() { } - /// - /// Create ApplicationConfigException - /// - /// Exception information - /// Exception context - protected ApplicationConfigException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } - /// /// Create ApplicationConfigException /// diff --git a/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs b/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs index 9619e9632..2fb071f33 100644 --- a/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs +++ b/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs @@ -16,6 +16,7 @@ public class FrontendFeatures : IFrontendFeatures public FrontendFeatures(IFeatureManager featureManager) { features.Add("footer", true); + features.Add("processActions", true); if (featureManager.IsEnabledAsync(FeatureFlags.JsonObjectInDataResponse).Result) { diff --git a/src/Altinn.App.Core/Internal/App/IAppEvents.cs b/src/Altinn.App.Core/Internal/App/IAppEvents.cs new file mode 100644 index 000000000..4ca2cb84b --- /dev/null +++ b/src/Altinn.App.Core/Internal/App/IAppEvents.cs @@ -0,0 +1,25 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.App; + +/// +/// Interface for implementing a receiver handling instance events. +/// +public interface IAppEvents +{ + /// + /// Callback on first start event of process. + /// + /// Start event to start + /// Instance data + /// + public Task OnStartAppEvent(string startEvent, Instance instance); + + /// + /// Is called when the process for an instance is ended. + /// + /// End event to end + /// Instance data + /// + public Task OnEndAppEvent(string endEvent, Instance instance); +} diff --git a/src/Altinn.App.Core/Internal/App/IAppResources.cs b/src/Altinn.App.Core/Internal/App/IAppResources.cs new file mode 100644 index 000000000..69cf19c57 --- /dev/null +++ b/src/Altinn.App.Core/Internal/App/IAppResources.cs @@ -0,0 +1,180 @@ +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Layout; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.App +{ + /// + /// Interface for execution functionality + /// + public interface IAppResources + { + /// + /// Get the app resource for the given parameters. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// the resource. + /// The app resource. + byte[] GetAppResource(string org, string app, string resource); + + /// + /// Get the app resource for the given parameters. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// the resource. + /// The app resource. + byte[] GetText(string org, string app, string textResource); + + /// + /// Get the text resources in a specific language. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The two letter language code. + /// The text resources in the specified language if they exist. Otherwise null. + Task GetTexts(string org, string app, string language); + + /// + /// Returns the model metadata for an app. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The ServiceMetadata for an app. + [Obsolete("GetModelMetaDataJSON is no longer used by app frontend. Use GetModelJsonSchema.")] + string GetModelMetaDataJSON(string org, string app); + + /// + /// Returns the json schema for the provided model id. + /// + /// Unique identifier for the model. + /// The JSON schema for the model + string GetModelJsonSchema(string modelId); + + /// + /// Method that fetches the runtime resources stored in wwwroot + /// + /// the resource + /// The filestream for the resource file + byte[]? GetRuntimeResource(string resource); + + /// + /// Returns the application metadata for an application. + /// + /// The application metadata for an application. + [Obsolete("GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationMetadata instead", false)] + Application GetApplication(); + + /// + /// Returns the application XACML policy for an application. + /// + /// The application XACML policy for an application. + [Obsolete("GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationXACMLPolicy instead", false)] + string? GetApplicationXACMLPolicy(); + + /// + /// Returns the application BPMN process for an application. + /// + /// The application BPMN process for an application. + [Obsolete("GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationBPMNProcess instead", false)] + string? GetApplicationBPMNProcess(); + + /// + /// Gets the prefill json file + /// + /// the data model name + /// The prefill json file as a string + string? GetPrefillJson(string dataModelName = "ServiceModel"); + + /// + /// Get the class ref based on data type + /// + /// The datatype + /// Returns the class ref for a given datatype. An empty string is returned if no match is found. + string GetClassRefForLogicDataType(string dataType); + + /// + /// Gets the layouts for the app. + /// + /// A dictionary of FormLayout objects serialized to JSON + string GetLayouts(); + + /// + /// Gets the the layouts settings + /// + /// The layout settings as a JSON string + string? GetLayoutSettingsString(); + + /// + /// Gets the layout settings + /// + /// The layout settings + LayoutSettings GetLayoutSettings(); + + /// + /// Gets the the layout sets + /// + /// The layout sets + string GetLayoutSets(); + + /// + /// Gets the footer layout + /// + /// The footer layout + Task GetFooter(); + + /// + /// Get the layout set definition. Return null if no layoutsets exists + /// + LayoutSets? GetLayoutSet(); + + /// + /// + /// + LayoutSet? GetLayoutSetForTask(string taskId); + + /// + /// Gets the layouts for av given layoutset + /// + /// The layot set id + /// A dictionary of FormLayout objects serialized to JSON + string GetLayoutsForSet(string layoutSetId); + + /// + /// Gets the full layout model for the optional set + /// + LayoutModel GetLayoutModel(string? layoutSetId = null); + + /// + /// Gets the the layouts settings for a layoutset + /// + /// The layot set id + /// The layout settings as a JSON string + string? GetLayoutSettingsStringForSet(string layoutSetId); + + /// + /// Gets the the layouts settings for a layoutset + /// + /// The layout settings + LayoutSettings? GetLayoutSettingsForSet(string? layoutSetId); + + /// + /// Gets the ruleconfiguration for av given layoutset + /// + /// A dictionary of FormLayout objects serialized to JSON + byte[] GetRuleConfigurationForSet(string id); + + /// + /// Gets the the rule handler for a layoutset + /// + /// The layout settings + byte[] GetRuleHandlerForSet(string id); + + /// + /// Gets the the rule handler for a layoutset + /// + /// The layout settings + string? GetValidationConfiguration(string modelId); + } +} diff --git a/src/Altinn.App.Core/Internal/App/IApplicationClient.cs b/src/Altinn.App.Core/Internal/App/IApplicationClient.cs new file mode 100644 index 000000000..dd8859081 --- /dev/null +++ b/src/Altinn.App.Core/Internal/App/IApplicationClient.cs @@ -0,0 +1,17 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.App +{ + /// + /// Interface for retrieving application metadata data related operations + /// + public interface IApplicationClient + { + /// + /// Gets the application metdata + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + Task GetApplication(string org, string app); + } +} diff --git a/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs b/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs new file mode 100644 index 000000000..c76e0af62 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs @@ -0,0 +1,89 @@ +using System.Security.Claims; +using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Internal.Process.Action; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Models; +using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Auth; + +/// +/// Service that handles authorization. Uses AuthorizationClient to communicate with authorization component. Makes authorization decisions in app context possible +/// +public class AuthorizationService : IAuthorizationService +{ + private readonly IAuthorizationClient _authorizationClient; + private readonly IEnumerable _userActionAuthorizers; + + /// + /// Initializes a new instance of the class + /// + /// The authorization client + /// The user action authorizers + public AuthorizationService(IAuthorizationClient authorizationClient, IEnumerable userActionAuthorizers) + { + _authorizationClient = authorizationClient; + _userActionAuthorizers = userActionAuthorizers; + } + + /// + public async Task?> GetPartyList(int userId) + { + return await _authorizationClient.GetPartyList(userId); + } + + /// + public async Task ValidateSelectedParty(int userId, int partyId) + { + return await _authorizationClient.ValidateSelectedParty(userId, partyId); + } + + /// + public async Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, ClaimsPrincipal user, string action, string? taskId = null) + { + if (!await _authorizationClient.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)) + { + return false; + } + + foreach (var authorizerRegistrator in _userActionAuthorizers.Where(a => IsAuthorizerForTaskAndAction(a, taskId, action))) + { + var context = new UserActionAuthorizerContext(user, instanceIdentifier, taskId, action); + if (!await authorizerRegistrator.Authorizer.AuthorizeAction(context)) + { + return false; + } + } + + return true; + } + + /// + public async Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions) + { + var authDecisions = await _authorizationClient.AuthorizeActions(instance, user, actions.Select(a => a.Value).ToList()); + List authorizedActions = new(); + foreach (var action in actions) + { + authorizedActions.Add(new UserAction() + { + Id = action.Value, + Authorized = authDecisions[action.Value], + ActionType = action.ActionType + }); + + } + + return authorizedActions; + } + + private static bool IsAuthorizerForTaskAndAction(IUserActionAuthorizerProvider authorizer, string? taskId, string action) + { + return (authorizer.TaskId == null && authorizer.Action == null) + || (authorizer.TaskId == null && authorizer.Action == action) + || (authorizer.TaskId == taskId && authorizer.Action == null) + || (authorizer.TaskId == taskId && authorizer.Action == action); + } +} diff --git a/src/Altinn.App.Core/Internal/Auth/IAuthenticationClient.cs b/src/Altinn.App.Core/Internal/Auth/IAuthenticationClient.cs new file mode 100644 index 000000000..061d7fa33 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Auth/IAuthenticationClient.cs @@ -0,0 +1,14 @@ +namespace Altinn.App.Core.Internal.Auth +{ + /// + /// Authentication interface. + /// + public interface IAuthenticationClient + { + /// + /// Refreshes the AltinnStudioRuntime JwtToken. + /// + /// Response message from Altinn Platform with refreshed token. + Task RefreshToken(); + } +} diff --git a/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs b/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs new file mode 100644 index 000000000..41fc248bd --- /dev/null +++ b/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs @@ -0,0 +1,50 @@ +using System.Security.Claims; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Models; +using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Auth +{ + /// + /// Interface for authorization functionality. + /// + public interface IAuthorizationClient + { + /// + /// Returns the list of parties that user has any rights for. + /// + /// The userId. + /// List of parties. + Task?> GetPartyList(int userId); + + /// + /// Verifies that the selected party is contained in the user's party list. + /// + /// The user id. + /// The party id. + /// Boolean indicating whether or not the user can represent the selected party. + Task ValidateSelectedParty(int userId, int partyId); + + /// + /// Check if the user is authorized to perform the given action on the given instance. + /// + /// + /// + /// + /// + /// + /// + Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, ClaimsPrincipal user, string action, string? taskId = null); + + /// + /// Check if the user is authorized to perform the given actions on the given instance. + /// + /// + /// + /// + /// + Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions); + } +} diff --git a/src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs b/src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs new file mode 100644 index 000000000..852467a7c --- /dev/null +++ b/src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs @@ -0,0 +1,50 @@ +using System.Security.Claims; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Models; +using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Auth +{ + /// + /// Interface for authorization functionality. + /// + public interface IAuthorizationService + { + /// + /// Returns the list of parties that user has any rights for. + /// + /// The userId. + /// List of parties. + Task?> GetPartyList(int userId); + + /// + /// Verifies that the selected party is contained in the user's party list. + /// + /// The user id. + /// The party id. + /// Boolean indicating whether or not the user can represent the selected party. + Task ValidateSelectedParty(int userId, int partyId); + + /// + /// Check if the user is authorized to perform the given action on the given instance. + /// + /// + /// + /// + /// + /// + /// + Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, ClaimsPrincipal user, string action, string? taskId = null); + + /// + /// Check if the user is authorized to perform the given actions on the given instance. + /// + /// + /// + /// + /// Dictionary with actions and the auth decision + Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions); + } +} diff --git a/src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs b/src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs new file mode 100644 index 000000000..31d873fd0 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs @@ -0,0 +1,18 @@ +#nullable enable + +namespace Altinn.App.Core.Internal.Auth +{ + /// + /// Defines the methods required for an implementation of a user JSON Web Token provider. + /// The provider is used by client implementations that needs the user token in requests + /// against other systems. + /// + public interface IUserTokenProvider + { + /// + /// Defines a method that can return a JSON Web Token of the current user. + /// + /// The Json Web Token for the current user. + public string GetUserToken(); + } +} diff --git a/src/Altinn.App.Core/Internal/Data/IDataClient.cs b/src/Altinn.App.Core/Internal/Data/IDataClient.cs new file mode 100644 index 000000000..61f361e19 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Data/IDataClient.cs @@ -0,0 +1,171 @@ +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.AspNetCore.Http; + +namespace Altinn.App.Core.Internal.Data +{ + /// + /// Interface for data handling + /// + public interface IDataClient + { + /// + /// Stores the form model + /// + /// The type + /// The app model to serialize + /// The instance id + /// The type for serialization + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The data type to create, must be a valid data type defined in application metadata + Task InsertFormData(T dataToSerialize, Guid instanceGuid, Type type, string org, string app, int instanceOwnerPartyId, string dataType); + + /// + /// Stores the form + /// + /// The model type + /// The instance that the data element belongs to + /// The data type with requirements + /// The data element instance + /// The class type describing the data + /// The data element metadata + Task InsertFormData(Instance instance, string dataType, T dataToSerialize, Type type); + + /// + /// updates the form data + /// + /// The type + /// The form data to serialize + /// The instanceid + /// The type for serialization + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// the data id + Task UpdateData(T dataToSerialize, Guid instanceGuid, Type type, string org, string app, int instanceOwnerPartyId, Guid dataId); + + /// + /// Gets the form data + /// + /// The instanceid + /// The type for serialization + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// the data id + Task GetFormData(Guid instanceGuid, Type type, string org, string app, int instanceOwnerPartyId, Guid dataId); + + /// + /// Gets the data as is. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instanceid + /// the data id + Task GetBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataId); + + /// + /// Method that gets metadata on form attachments ordered by attachmentType + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// A list with attachments metadata ordered by attachmentType + Task> GetBinaryDataList(string org, string app, int instanceOwnerPartyId, Guid instanceGuid); + + /// + /// Method that removes a form attachments from disk/storage + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The attachment id + [Obsolete("Use method DeleteData with delayed=false instead.", error: true)] + Task DeleteBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid); + + /// + /// Method that removes a data elemen from disk/storage immediatly or marks it as deleted. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The attachment id + /// A boolean indicating whether or not the delete should be executed immediately or delayed + Task DeleteData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid, bool delay); + + /// + /// Method that saves a form attachments to disk/storage and returns the new data element. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The data type to create, must be a valid data type defined in application metadata + /// Http request containing the attachment to be saved + Task InsertBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, string dataType, HttpRequest request); + + /// + /// Method that updates a form attachments to disk/storage and returns the updated data element. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The data id + /// Http request containing the attachment to be saved + [Obsolete(message:"Deprecated please use UpdateBinaryData(InstanceIdentifier, string, string, Guid, Stream) instead", error: false)] + Task UpdateBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid, HttpRequest request); + + /// + /// Method that updates a form attachments to disk/storage and returns the updated data element. + /// + /// Instance identifier instanceOwnerPartyId and instanceGuid + /// Content type of the updated binary data + /// Filename of the updated binary data + /// Guid of the data element to update + /// Updated binary data + Task UpdateBinaryData(InstanceIdentifier instanceIdentifier, string? contentType, string filename, Guid dataGuid, Stream stream); + + /// + /// Insert a binary data element. + /// + /// isntanceId = {instanceOwnerPartyId}/{instanceGuid} + /// data type + /// content type + /// filename + /// the stream to stream + /// Optional field to set what task the binary data was generated from + /// + Task InsertBinaryData(string instanceId, string dataType, string contentType, string? filename, Stream stream, string? generatedFromTask = null); + + /// + /// Updates the data element metadata object. + /// + /// The instance which is not updated + /// The data element with values to update + /// the updated data element + Task Update(Instance instance, DataElement dataElement); + + /// + /// Lock data element in storage + /// + /// InstanceIdentifier identifying the instance containing the DataElement to lock + /// Id of the DataElement to lock + /// + Task LockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid); + + /// + /// Unlock data element in storage + /// + /// InstanceIdentifier identifying the instance containing the DataElement to unlock + /// Id of the DataElement to unlock + /// + Task UnlockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid); + } +} diff --git a/src/Altinn.App.Core/Internal/Events/IEventsClient.cs b/src/Altinn.App.Core/Internal/Events/IEventsClient.cs new file mode 100644 index 000000000..5888ec778 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Events/IEventsClient.cs @@ -0,0 +1,15 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Events +{ + /// + /// Interface describing client implementations for the Events component in the Altinn 3 platform. + /// + public interface IEventsClient + { + /// + /// Adds a new event to the events published by the Events component. + /// + Task AddEvent(string eventType, Instance instance); + } +} diff --git a/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs b/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs index a8bf5234d..c204b574c 100644 --- a/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs +++ b/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Secrets; namespace Altinn.App.Core.Internal.Events { @@ -8,7 +8,7 @@ namespace Altinn.App.Core.Internal.Events /// public class KeyVaultEventSecretCodeProvider : IEventSecretCodeProvider { - private readonly ISecrets _keyVaultClient; + private readonly ISecretsClient _keyVaultClient; private string _secretCode = string.Empty; /// @@ -16,7 +16,7 @@ public class KeyVaultEventSecretCodeProvider : IEventSecretCodeProvider /// This /// /// - public KeyVaultEventSecretCodeProvider(ISecrets keyVaultClient) + public KeyVaultEventSecretCodeProvider(ISecretsClient keyVaultClient) { _keyVaultClient = keyVaultClient; } diff --git a/src/Altinn.App.Core/Internal/Exceptions/NotFoundException.cs b/src/Altinn.App.Core/Internal/Exceptions/NotFoundException.cs new file mode 100644 index 000000000..757c3fb7b --- /dev/null +++ b/src/Altinn.App.Core/Internal/Exceptions/NotFoundException.cs @@ -0,0 +1,15 @@ +namespace Altinn.App.Core.Internal.Exceptions; + +/// +/// Exception thrown when a resource is not found +/// +public class NotFoundException: Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// + public NotFoundException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs index cb4657e4b..86bb9fcda 100644 --- a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs +++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs @@ -47,7 +47,7 @@ public static bool EvaluateBooleanExpression(LayoutEvaluatorState state, Compone /// /// Evaluate a from a given in a /// - public static object? EvaluateExpression(LayoutEvaluatorState state, Expression expr, ComponentContext context) + public static object? EvaluateExpression(LayoutEvaluatorState state, Expression expr, ComponentContext? context, object[]? positionalArguments = null) { if (expr is null) { @@ -58,10 +58,10 @@ public static bool EvaluateBooleanExpression(LayoutEvaluatorState state, Compone return expr.Value; } - var args = expr.Args.Select(a => EvaluateExpression(state, a, context)).ToArray(); + var args = expr.Args.Select(a => EvaluateExpression(state, a, context, positionalArguments)).ToArray(); var ret = expr.Function switch { - ExpressionFunction.dataModel => state.GetModelData(args.First()?.ToString(), context), + ExpressionFunction.dataModel => DataModel(args.First()?.ToString(), context, state), ExpressionFunction.component => Component(args, context, state), ExpressionFunction.instanceContext => state.GetInstanceContext(args.First()?.ToString()!), ExpressionFunction.@if => IfImpl(args), @@ -85,12 +85,27 @@ public static bool EvaluateBooleanExpression(LayoutEvaluatorState state, Compone ExpressionFunction.round => Round(args), ExpressionFunction.upperCase => UpperCase(args), ExpressionFunction.lowerCase => LowerCase(args), + ExpressionFunction.argv => Argv(args, positionalArguments), + ExpressionFunction.gatewayAction => state.GetGatewayAction(), _ => throw new ExpressionEvaluatorTypeErrorException($"Function \"{expr.Function}\" not implemented"), }; return ret; } - private static object? Component(object?[] args, ComponentContext context, LayoutEvaluatorState state) + private static object? DataModel(string? key, ComponentContext? context, LayoutEvaluatorState state) + { + var data = state.GetModelData(key, context); + + // Only allow IConvertible types to be returned from data model + // Objects and arrays should return null + return data switch + { + IConvertible c => c, + _ => null, + }; + } + + private static object? Component(object?[] args, ComponentContext? context, LayoutEvaluatorState state) { var componentId = args.First()?.ToString(); if (componentId is null) @@ -98,6 +113,11 @@ public static bool EvaluateBooleanExpression(LayoutEvaluatorState state, Compone throw new ArgumentException("Cannot lookup component null"); } + if (context is null) + { + throw new ArgumentException("The component expression requires a component context"); + } + var targetContext = state.GetComponentContext(context.Component.PageId, componentId, context.RowIndices); if (targetContext.Component is GroupComponent) @@ -120,7 +140,7 @@ public static bool EvaluateBooleanExpression(LayoutEvaluatorState state, Compone parent = parent.Parent; } - return state.GetModelData(binding, context); + return DataModel(binding, context, state); } private static string? Concat(object?[] args) @@ -331,9 +351,7 @@ private static (double?, double?) PrepareNumericArgs(object?[] args) { bool ab => throw new ExpressionEvaluatorTypeErrorException($"Expected number, got value {(ab ? "true" : "false")}"), string s => parseNumber(s), - int i => Convert.ToDouble(i), - decimal d => Convert.ToDouble(d), - object o => o as double?, // assume all relevant numbers are representable as double (as in frontend) + IConvertible c => Convert.ToDouble(c), _ => null }; } @@ -469,5 +487,30 @@ private static (double?, double?) PrepareNumericArgs(object?[] args) return string.Equals(ToStringForEquals(args[0]), ToStringForEquals(args[1]), StringComparison.InvariantCulture); } + + private static object Argv(object?[] args, object[]? positionalArguments) + { + if (args.Length != 1) + { + throw new ExpressionEvaluatorTypeErrorException($"Expected 1 argument(s), got {args.Length}"); + } + + var index = (int?)PrepareNumericArg(args[0]); + if (!index.HasValue) + { + throw new ExpressionEvaluatorTypeErrorException($"Expected number, got value \"{args[0]}\""); + } + + if (positionalArguments == null) + { + throw new ExpressionEvaluatorTypeErrorException("No positional arguments available"); + } + if (index < 0 || index >= positionalArguments.Length) + { + throw new ExpressionEvaluatorTypeErrorException($"Index {index} out of range"); + } + + return positionalArguments[index.Value]; + } } diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluatorTypeErrorException.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluatorTypeErrorException.cs index 4d86b1a75..bcb87bc3e 100644 --- a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluatorTypeErrorException.cs +++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluatorTypeErrorException.cs @@ -5,14 +5,10 @@ namespace Altinn.App.Core.Internal.Expressions; /// /// Custom exception for to thow when expressions contains type errors. /// -[Serializable] public class ExpressionEvaluatorTypeErrorException : Exception { /// public ExpressionEvaluatorTypeErrorException(string msg) : base(msg) {} /// public ExpressionEvaluatorTypeErrorException(string msg, Exception innerException) : base(msg, innerException) {} - - /// - protected ExpressionEvaluatorTypeErrorException(SerializationInfo info, StreamingContext ctxt) : base(info, ctxt) { } } \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluator.cs b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluator.cs index 6d972cc31..a736adb55 100644 --- a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluator.cs +++ b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluator.cs @@ -102,7 +102,7 @@ public static void RemoveHiddenData(LayoutEvaluatorState state, RowRemovalOption /// /// Return a list of for the given state and dataElementId /// - public static IEnumerable RunLayoutValidationsForRequired(LayoutEvaluatorState state, string dataElementId) + public static List RunLayoutValidationsForRequired(LayoutEvaluatorState state, string dataElementId) { var validationIssues = new List(); @@ -134,7 +134,6 @@ private static void RunLayoutValidationsForRequiredRecurs(List validationIssues.Add(new ValidationIssue() { Severity = ValidationIssueSeverity.Error, - InstanceId = state.GetInstanceContext("instanceId").ToString(), DataElementId = dataElementId, Field = field, Description = $"{field} is required in component with id {context.Component.Id}", diff --git a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs index fd7e9f953..c376e5341 100644 --- a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs +++ b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs @@ -16,17 +16,19 @@ public class LayoutEvaluatorState private readonly LayoutModel _componentModel; private readonly FrontEndSettings _frontEndSettings; private readonly Instance _instanceContext; + private readonly string? _gatewayAction; private readonly ComponentContext[]? _pageContexts; /// /// Constructor for LayoutEvaluatorState. Usually called via that can be fetched from dependency injection. /// - public LayoutEvaluatorState(IDataModelAccessor dataModel, LayoutModel componentModel, FrontEndSettings frontEndSettings, Instance instance) + public LayoutEvaluatorState(IDataModelAccessor dataModel, LayoutModel componentModel, FrontEndSettings frontEndSettings, Instance instance, string? gatewayAction = null) { _dataModel = dataModel; _componentModel = componentModel; _frontEndSettings = frontEndSettings; _instanceContext = instance; + _gatewayAction = gatewayAction; if (dataModel is not null && componentModel is not null) { @@ -177,6 +179,14 @@ public ComponentContext GetComponentContext(string pageName, string componentId, return _dataModel.GetModelData(key, context?.RowIndices); } + /// + /// Get all of the resolved keys (including all possible indexes) from a data model key + /// + public string[] GetResolvedKeys(string key) + { + return _dataModel.GetResolvedKeys(key); + } + /// /// Set the value of a field to null. /// @@ -210,6 +220,15 @@ public string GetInstanceContext(string key) }; } + /// + /// Get the gateway action from the instance context + /// + /// Returns null if no action defined + public string? GetGatewayAction() + { + return _gatewayAction; + } + /// /// Return a full dataModelBiding from a context aware binding by adding indicies /// diff --git a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs index d526737dc..db8a88c95 100644 --- a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs +++ b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs @@ -1,8 +1,6 @@ -using System.Text.Json; -using Altinn.App.Core.Interface; using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers.DataModel; -using Altinn.App.Core.Models.Layout; +using Altinn.App.Core.Internal.App; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Options; @@ -29,9 +27,9 @@ public LayoutEvaluatorStateInitializer(IAppResources appResources, IOptions /// Initialize LayoutEvaluatorState with given Instance, data object and layoutSetId /// - public Task Init(Instance instance, object data, string? layoutSetId) + public virtual Task Init(Instance instance, object data, string? layoutSetId, string? gatewayAction = null) { var layouts = _appResources.GetLayoutModel(layoutSetId); - return Task.FromResult(new LayoutEvaluatorState(new DataModel(data), layouts, _frontEndSettings, instance)); + return Task.FromResult(new LayoutEvaluatorState(new DataModel(data), layouts, _frontEndSettings, instance, gatewayAction)); } } diff --git a/src/Altinn.App.Core/Internal/Instances/IInstanceClient.cs b/src/Altinn.App.Core/Internal/Instances/IInstanceClient.cs new file mode 100644 index 000000000..e64640bb7 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Instances/IInstanceClient.cs @@ -0,0 +1,135 @@ +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Primitives; + +namespace Altinn.App.Core.Internal.Instances +{ + /// + /// Interface for handling form data related operations + /// + public interface IInstanceClient + { + /// + /// Gets the instance + /// + Task GetInstance(string app, string org, int instanceOwnerPartyId, Guid instanceId); + + /// + /// Gets the instance anew. Instance must have set appId, instanceOwner.PartyId and Id. + /// + Task GetInstance(Instance instance); + + /// + /// Gets a list of instances based on a dictionary of provided query parameters. + /// + Task> GetInstances(Dictionary queryParams); + + /// + /// Updates the process model of the instance and returns the updated instance. + /// + Task UpdateProcess(Instance instance); + + /// + /// Creates an instance of an application with no data. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// the instance template to create (must have instanceOwner with partyId, personNumber or organisationNumber set) + /// The created instance + Task CreateInstance(string org, string app, Instance instanceTemplate); + + /// + /// Add complete confirmation. + /// + /// + /// Add to an instance that a given stakeholder considers the instance as no longer needed by them. The stakeholder has + /// collected all the data and information they needed from the instance and expect no additional data to be added to it. + /// The body of the request isn't used for anything despite this being a POST operation. + /// + /// The party id of the instance owner. + /// The id of the instance to confirm as complete. + /// Returns the updated instance. + Task AddCompleteConfirmation(int instanceOwnerPartyId, Guid instanceGuid); + + /// + /// Update read status. + /// + /// The party id of the instance owner. + /// The id of the instance to confirm as complete. + /// The new instance read status. + /// Returns the updated instance. + Task UpdateReadStatus(int instanceOwnerPartyId, Guid instanceGuid, string readStatus); + + /// + /// Update substatus. + /// + /// The party id of the instance owner. + /// The id of the instance to be updated. + /// The new substatus. + /// Returns the updated instance. + Task UpdateSubstatus(int instanceOwnerPartyId, Guid instanceGuid, Substatus substatus); + + /// + /// Update presentation texts. + /// + /// + /// The provided presentation texts will be merged with the existing collection of presentation texts on the instance. + /// + /// The party id of the instance owner. + /// The id of the instance to update presentation texts for. + /// The presentation texts + /// Returns the updated instance. + Task UpdatePresentationTexts(int instanceOwnerPartyId, Guid instanceGuid, PresentationTexts presentationTexts); + + /// + /// Update data values. + /// + /// + /// The provided data values will be merged with the existing collection of data values on the instance. + /// + /// The party id of the instance owner. + /// The id of the instance to update data values for. + /// The data values + /// Returns the updated instance. + Task UpdateDataValues(int instanceOwnerPartyId, Guid instanceGuid, DataValues dataValues); + + /// + /// Update data data values. + /// + /// + /// The provided data value will be added with the existing collection of data values on the instance. + /// + /// The instance + /// The data value (null unsets the value) + /// Returns the updated instance. + async Task UpdateDataValues(Instance instance, Dictionary dataValues) + { + var id = new InstanceIdentifier(instance); + return await UpdateDataValues(id.InstanceOwnerPartyId, id.InstanceGuid, new DataValues{Values = dataValues}); + } + + /// + /// Update single data value. + /// + /// + /// The provided data value will be added with the existing collection of data values on the instance. + /// + /// The instance + /// The key of the DataValues collection to be updated. + /// The data value (null unsets the value) + /// Returns the updated instance. + async Task UpdateDataValue(Instance instance, string key, string? value) + { + return await UpdateDataValues(instance, new Dictionary{{key, value}}); + } + + /// + /// Delete instance. + /// + /// The party id of the instance owner. + /// The id of the instance to delete. + /// Boolean to indicate if instance should be hard deleted. + /// Returns the deleted instance. + Task DeleteInstance(int instanceOwnerPartyId, Guid instanceGuid, bool hard); + } +} diff --git a/src/Altinn.App.Core/Internal/Instances/IInstanceEventClient.cs b/src/Altinn.App.Core/Internal/Instances/IInstanceEventClient.cs new file mode 100644 index 000000000..1b4f4e81a --- /dev/null +++ b/src/Altinn.App.Core/Internal/Instances/IInstanceEventClient.cs @@ -0,0 +1,20 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Instances +{ + /// + /// Interface for handling instance event related operations + /// + public interface IInstanceEventClient + { + /// + /// Stores the instance event + /// + Task SaveInstanceEvent(object dataToSerialize, string org, string app); + + /// + /// Gets the instance events related to the instance matching the instance id. + /// + Task> GetInstanceEvents(string instanceId, string instanceOwnerPartyId, string org, string app, string[] eventTypes, string from, string to); + } +} diff --git a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs index 1a93ea185..3c300aa1a 100644 --- a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs @@ -21,7 +21,8 @@ public interface IPdfService /// to storage as a new binary file associated with the predefined PDF data type in most apps. /// /// The instance details. + /// The task id for witch the pdf is generated /// Cancellation Token for when a request should be stopped before it's completed. - Task GenerateAndStorePdf(Instance instance, CancellationToken ct); + Task GenerateAndStorePdf(Instance instance, string taskId, CancellationToken ct); } -} \ No newline at end of file +} diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs b/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs index 331c66d25..6daf76420 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs @@ -5,7 +5,6 @@ namespace Altinn.App.Core.Internal.Pdf /// /// Class representing an exception throw when a PDF could not be created. /// - [Serializable] public class PdfGenerationException : Exception { /// @@ -31,13 +30,5 @@ public PdfGenerationException(string? message) : base(message) public PdfGenerationException(string? message, Exception? innerException) : base(message, innerException) { } - - /// - /// Creates a new Exception of - /// Intended to be used when the generation of PDF fails. - /// - protected PdfGenerationException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } } } diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index de1251bd4..b49104469 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -1,11 +1,13 @@ using System.Security.Claims; -using System.Text; using System.Xml.Serialization; using Altinn.App.Core.Configuration; using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; using Altinn.App.Core.Helpers.Extensions; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; @@ -27,10 +29,10 @@ public class PdfService : IPdfService private readonly IPDF _pdfClient; private readonly IAppResources _resourceService; private readonly IPdfOptionsMapping _pdfOptionsMapping; - private readonly IData _dataClient; + private readonly IDataClient _dataClient; private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IProfile _profileClient; - private readonly IRegister _registerClient; + private readonly IProfileClient _profileClient; + private readonly IAltinnPartyClient _altinnPartyClientClient; private readonly IPdfFormatter _pdfFormatter; private readonly IPdfGeneratorClient _pdfGeneratorClient; @@ -49,7 +51,7 @@ public class PdfService : IPdfService /// The data client. /// The httpContextAccessor /// The profile client - /// The register client + /// The register client /// Class for customizing pdf formatting and layout. /// PDF generator client for the experimental PDF generator service /// PDF generator related settings. @@ -58,15 +60,14 @@ public PdfService( IPDF pdfClient, IAppResources appResources, IPdfOptionsMapping pdfOptionsMapping, - IData dataClient, + IDataClient dataClient, IHttpContextAccessor httpContextAccessor, - IProfile profileClient, - IRegister registerClient, + IProfileClient profileClient, + IAltinnPartyClient altinnPartyClientClient, IPdfFormatter pdfFormatter, IPdfGeneratorClient pdfGeneratorClient, IOptions pdfGeneratorSettings, - IOptions generalSettings - ) + IOptions generalSettings) { _pdfClient = pdfClient; _resourceService = appResources; @@ -74,7 +75,7 @@ IOptions generalSettings _dataClient = dataClient; _httpContextAccessor = httpContextAccessor; _profileClient = profileClient; - _registerClient = registerClient; + _altinnPartyClientClient = altinnPartyClientClient; _pdfFormatter = pdfFormatter; _pdfGeneratorClient = pdfGeneratorClient; _pdfGeneratorSettings = pdfGeneratorSettings.Value; @@ -83,7 +84,7 @@ IOptions generalSettings /// - public async Task GenerateAndStorePdf(Instance instance, CancellationToken ct) + public async Task GenerateAndStorePdf(Instance instance, string taskId, CancellationToken ct) { var baseUrl = _generalSettings.FormattedExternalAppBaseUrl(new AppIdentifier(instance)); var pagePath = _pdfGeneratorSettings.AppPdfPagePathTemplate.ToLowerInvariant().Replace("{instanceid}", instance.Id); @@ -100,13 +101,13 @@ public async Task GenerateAndStorePdf(Instance instance, CancellationToken ct) TextResource? textResource = await GetTextResource(appIdentifier.App, appIdentifier.Org, language); string fileName = GetFileName(instance, textResource); - await _dataClient.InsertBinaryData( instance.Id, PdfElementType, PdfContentType, fileName, - pdfContent); + pdfContent, + taskId); } private static Uri BuildUri(string baseUrl, string pagePath, string language) @@ -191,7 +192,7 @@ public async Task GenerateAndStoreReceiptPDF(Instance instance, string taskId, D else { string? orgNumber = user.GetOrgNumber().ToString(); - actingParty = await _registerClient.LookupParty(new PartyLookup { OrgNo = orgNumber }); + actingParty = await _altinnPartyClientClient.LookupParty(new PartyLookup { OrgNo = orgNumber }); } // If layoutset exists pick correct layotFiles @@ -216,18 +217,18 @@ public async Task GenerateAndStoreReceiptPDF(Instance instance, string taskId, D LayoutSettings = layoutSettings, TextResources = JsonConvert.DeserializeObject(textResourcesString)!, OptionsDictionary = optionsDictionary, - Party = await _registerClient.GetParty(instanceOwnerId), + Party = await _altinnPartyClientClient.GetParty(instanceOwnerId), Instance = instance, UserParty = actingParty, Language = language }; Stream pdfContent = await _pdfClient.GeneratePDF(pdfContext); - await StorePDF(pdfContent, instance, textResource); + await StorePDF(pdfContent, instance, textResource, taskId); pdfContent.Dispose(); } - private async Task StorePDF(Stream pdfStream, Instance instance, TextResource textResource) + private async Task StorePDF(Stream pdfStream, Instance instance, TextResource textResource, string generatedFromTask) { string? fileName = null; string app = instance.AppId.Split("/")[1]; @@ -245,7 +246,8 @@ private async Task StorePDF(Stream pdfStream, Instance instance, Te PdfElementType, PdfContentType, fileName, - pdfStream); + pdfStream, + generatedFromTask); } private async Task GetLanguage() diff --git a/src/Altinn.App.Core/Internal/Prefill/IPrefill.cs b/src/Altinn.App.Core/Internal/Prefill/IPrefill.cs new file mode 100644 index 000000000..3fcb13ae2 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Prefill/IPrefill.cs @@ -0,0 +1,25 @@ +namespace Altinn.App.Core.Internal.Prefill +{ + /// + /// The prefill service + /// + public interface IPrefill + { + /// + /// Prefills the data model based on key/values in the dictionary. + /// + /// The data model object + /// External given prefill + /// Ignore errors when true, throw on errors when false + void PrefillDataModel(object dataModel, Dictionary externalPrefill, bool continueOnError = false); + + /// + /// Prefills the data model based on the prefill json configuration file + /// + /// The partyId of the instance owner + /// The data model name + /// The data model object + /// External given prefill + Task PrefillDataModel(string partyId, string dataModelName, object dataModel, Dictionary? externalPrefill = null); + } +} diff --git a/src/Altinn.App.Core/Internal/Process/Action/IUserActionAuthorizerProvider.cs b/src/Altinn.App.Core/Internal/Process/Action/IUserActionAuthorizerProvider.cs new file mode 100644 index 000000000..49efd787e --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Action/IUserActionAuthorizerProvider.cs @@ -0,0 +1,24 @@ +using Altinn.App.Core.Features; + +namespace Altinn.App.Core.Internal.Process.Action; + +/// +/// Register a user action authorizer for a given action and/or task +/// +public interface IUserActionAuthorizerProvider +{ + /// + /// Gets or sets the action + /// + public string? Action { get; } + + /// + /// Gets or sets the task id + /// + public string? TaskId { get; } + + /// + /// Gets or sets the authorizer implementation + /// + public IUserActionAuthorizer Authorizer { get; } +} diff --git a/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerProvider.cs b/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerProvider.cs new file mode 100644 index 000000000..39b55dbf2 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerProvider.cs @@ -0,0 +1,32 @@ +using Altinn.App.Core.Features; + +namespace Altinn.App.Core.Internal.Process.Action; + +/// +/// Register a user action authorizer for a given action and/or task +/// +public class UserActionAuthorizerProvider: IUserActionAuthorizerProvider +{ + + /// + /// Initializes a new instance of the class + /// + /// + /// + /// + public UserActionAuthorizerProvider(string? taskId, string? action, IUserActionAuthorizer authorizer) + { + TaskId = taskId; + Action = action; + Authorizer = authorizer; + } + + /// + public string? Action { get; set; } + + /// + public string? TaskId { get; set; } + + /// + public IUserActionAuthorizer Authorizer { get; set; } +} diff --git a/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtension.cs b/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtension.cs new file mode 100644 index 000000000..dbb2a0bcf --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtension.cs @@ -0,0 +1,66 @@ +using Altinn.App.Core.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Altinn.App.Core.Internal.Process.Action; + +/// +/// Extension methods for adding user action authorizers to the service collection connected to a action and/or task +/// +public static class UserActionAuthorizerServiceCollectionExtension +{ + /// + /// Adds a transient user action authorizer to the service collection connected to a action and task + /// + /// ServiceCollection + /// Id of the task the authorizer should run for + /// Name of the action the authorizer should run for + /// Implementation if that should be executed + /// + public static IServiceCollection AddTransientUserActionAuthorizerForActionInTask(this IServiceCollection services, string taskId, string action) where T : class, IUserActionAuthorizer + { + return services.RegisterUserActionAuthorizer(taskId, action); + } + + /// + /// Adds a transient user action authorizer to the service collection connected to a action in all tasks + /// + /// ServiceCollection + /// Name of the action the authorizer should run for + /// Implementation if that should be executed + /// + public static IServiceCollection AddTransientUserActionAuthorizerForActionInAllTasks(this IServiceCollection services, string action) where T : class, IUserActionAuthorizer + { + return services.RegisterUserActionAuthorizer(null, action); + } + + /// + /// Adds a transient user action authorizer to the service collection connected to all actions in a task + /// + /// ServiceCollection + /// Name of the action the authorizer should run for + /// Implementation if that should be executed + /// + public static IServiceCollection AddTransientUserActionAuthorizerForAllActionsInTask(this IServiceCollection services, string taskId) where T : class, IUserActionAuthorizer + { + return services.RegisterUserActionAuthorizer(taskId, null); + } + + /// + /// Adds a transient user action authorizer to the service collection connected to all actions in all tasks + /// + /// ServiceCollection + /// Implementation if that should be executed + /// + public static IServiceCollection AddTransientUserActionAuthorizerForAllActionsInAllTasks(this IServiceCollection services) where T : class, IUserActionAuthorizer + { + return services.RegisterUserActionAuthorizer(null, null); + } + + private static IServiceCollection RegisterUserActionAuthorizer(this IServiceCollection services, string? taskId, string? action) where T : class, IUserActionAuthorizer + { + services.TryAddTransient(); + services.AddTransient(sp => new UserActionAuthorizerProvider(taskId, action, sp.GetRequiredService())); + return services; + } +} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs new file mode 100644 index 000000000..9a10e1427 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs @@ -0,0 +1,70 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using System.Xml.Serialization; + +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties +{ + /// + /// Defines an altinn action for a task + /// + public class AltinnAction + { + /// + /// Initializes a new instance of the class + /// + public AltinnAction() + { + Value = string.Empty; + ActionType = ActionType.ProcessAction; + } + /// + /// Initializes a new instance of the class with the given ID + /// + /// + public AltinnAction(string id) + { + Value = id; + ActionType = ActionType.ProcessAction; + } + + /// + /// Initializes a new instance of the class with the given ID and action type + /// + /// + /// + public AltinnAction(string id, ActionType actionType) + { + Value = id; + ActionType = actionType; + } + + /// + /// Gets or sets the ID of the action + /// + [XmlText] + public string Value { get; set; } + + /// + /// Gets or sets the type of action + /// + [XmlAttribute("type", Namespace = "http://altinn.no/process")] + public ActionType ActionType { get; set; } + } + + /// + /// Defines the different types of actions + /// + public enum ActionType + { + /// + /// The action is a process action + /// + [XmlEnum("processAction")] + ProcessAction, + /// + /// The action is a generic server action + /// + [XmlEnum("serverAction")] + ServerAction + } +} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnGatewayExtension.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnGatewayExtension.cs new file mode 100644 index 000000000..556a60f4d --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnGatewayExtension.cs @@ -0,0 +1,16 @@ +using System.Xml.Serialization; + +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties +{ + /// + /// Defines the altinn properties for a task + /// + public class AltinnGatewayExtension + { + /// + /// Gets or sets the data type id connected to the task + /// + [XmlElement("connectedDataTypeId", Namespace = "http://altinn.no/process", IsNullable = true)] + public string? ConnectedDataTypeId { get; set; } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnSignatureConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnSignatureConfiguration.cs new file mode 100644 index 000000000..c980c53fa --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnSignatureConfiguration.cs @@ -0,0 +1,29 @@ +using System.Xml.Serialization; + +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; + +/// +/// Configuration properties for signatures in a process task +/// +public class AltinnSignatureConfiguration +{ + /// + /// Define what taskId that should be signed for signing tasks + /// + [XmlArray(ElementName = "dataTypesToSign", Namespace = "http://altinn.no/process", IsNullable = true)] + [XmlArrayItem(ElementName = "dataType", Namespace = "http://altinn.no/process")] + public List DataTypesToSign { get; set; } = new(); + + /// + /// Set what dataTypeId that should be used for storing the signature + /// + [XmlElement("signatureDataType", Namespace = "http://altinn.no/process")] + public string SignatureDataType { get; set; } + + /// + /// Define what signature dataypes this signature should be unique from. Users that have sign any of the signatures in the list will not be able to sign this signature + /// + [XmlArray(ElementName = "uniqueFromSignaturesInDataTypes", Namespace = "http://altinn.no/process", IsNullable = true)] + [XmlArrayItem(ElementName = "dataType", Namespace = "http://altinn.no/process")] + public List UniqueFromSignaturesInDataTypes { get; set; } = new(); +} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs new file mode 100644 index 000000000..98ff963ee --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs @@ -0,0 +1,31 @@ +using System.Xml.Serialization; + +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties +{ + /// + /// Defines the altinn properties for a task + /// + public class AltinnTaskExtension + { + /// + /// List of available actions for a task + /// + [XmlArray(ElementName = "actions", Namespace = "http://altinn.no/process", IsNullable = true)] + [XmlArrayItem(ElementName = "action", Namespace = "http://altinn.no/process")] + public List? AltinnActions { get; set; } + + /// + /// Gets or sets the task type + /// + //[XmlElement(ElementName = "taskType", Namespace = "http://altinn.no/process/task", IsNullable = true)] + [XmlElement("taskType", Namespace = "http://altinn.no/process")] + public string? TaskType { get; set; } + + + /// + /// Gets or sets the configuration for signature + /// + [XmlElement("signatureConfig", Namespace = "http://altinn.no/process")] + public AltinnSignatureConfiguration? SignatureConfiguration { get; set; } = new AltinnSignatureConfiguration(); + } +} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs new file mode 100644 index 000000000..02e528321 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs @@ -0,0 +1,60 @@ +using System.Text.Json.Serialization; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Extended representation of a status object that holds the process state of an application instance. +/// +public class AppProcessElementInfo: ProcessElementInfo +{ + /// + /// Create a new instance of with no fields set. + /// + public AppProcessElementInfo() + { + Actions = new Dictionary(); + UserActions = new List(); + } + + /// + /// Create a new instance of with values copied from . + /// + /// The to copy values from. + public AppProcessElementInfo(ProcessElementInfo processElementInfo) + { + Flow = processElementInfo.Flow; + Started = processElementInfo.Started; + ElementId = processElementInfo.ElementId; + Name = processElementInfo.Name; + AltinnTaskType = processElementInfo.AltinnTaskType; + Ended = processElementInfo.Ended; + Validated = processElementInfo.Validated; + FlowType = processElementInfo.FlowType; + Actions = new Dictionary(); + UserActions = new List(); + } + /// + /// Actions that can be performed and if the user is allowed to perform them. + /// + [JsonPropertyName(name:"actions")] + public Dictionary? Actions { get; set; } + + /// + /// List of available actions for a task, both user and process tasks + /// + [JsonPropertyName(name:"userActions")] + public List UserActions { get; set; } + + /// + /// Indicates if the user has read access to the task. + /// + [JsonPropertyName(name:"read")] + public bool HasReadAccess { get; set; } + + /// + /// Indicates if the user has write access to the task. + /// + [JsonPropertyName(name:"write")] + public bool HasWriteAccess { get; set; } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessState.cs b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessState.cs new file mode 100644 index 000000000..eac09508a --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessState.cs @@ -0,0 +1,49 @@ +#nullable enable +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Extended representation of a status object that holds the process state of an application instance. +/// The process is defined by the application's process specification BPMN file. +/// +public class AppProcessState: ProcessState +{ + /// + /// Default constructor + /// + public AppProcessState() + { + } + + /// + /// Constructor that takes a ProcessState object and copies the values. + /// + /// + public AppProcessState(ProcessState? processState) + { + if(processState == null) + { + return; + } + Started = processState.Started; + StartEvent = processState.StartEvent; + if (processState.CurrentTask != null) + { + CurrentTask = new AppProcessElementInfo(processState.CurrentTask); + } + Ended = processState.Ended; + EndEvent = processState.EndEvent; + + } + /// + /// Gets or sets a status object containing the task info of the currentTask of an ongoing process. + /// + public new AppProcessElementInfo? CurrentTask { get; set; } + + /// + /// Gets or sets a list of all tasks. The list contains information about the task Id + /// and the task type. + /// + public List? ProcessTasks { get; set; } +} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs new file mode 100644 index 000000000..179af8761 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs @@ -0,0 +1,25 @@ +#nullable enable +using System.Xml.Serialization; +using System.Text.Json.Serialization; + +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Representation of a task's id and type. Used by the frontend to determine which tasks +/// exist, and their type. +/// +public class AppProcessTaskTypeInfo +{ + /// + /// Gets or sets the task type + /// + [XmlElement("altinnTaskType", Namespace = "http://altinn.no/process")] + public string? AltinnTaskType { get; set; } + + + /// + /// Gets or sets a reference to the current task/event element id as given in the process definition. + /// + [JsonPropertyName(name: "elementId")] + public string? ElementId { get; set; } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ConfirmationTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/ConfirmationTask.cs index fdcdaca36..8e823501f 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/ConfirmationTask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/ConfirmationTask.cs @@ -1,5 +1,4 @@ -using Altinn.App.Core.Interface; -using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.Elements { @@ -19,21 +18,21 @@ public ConfirmationTask(ITaskEvents taskEvents) } /// - public override async Task HandleTaskAbandon(ProcessChangeContext processChangeContext) + public override async Task HandleTaskAbandon(string elementId, Instance instance) { - await _taskEvents.OnAbandonProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance); + await _taskEvents.OnAbandonProcessTask(elementId, instance); } /// - public override async Task HandleTaskComplete(ProcessChangeContext processChangeContext) + public override async Task HandleTaskComplete(string elementId, Instance instance) { - await _taskEvents.OnEndProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance); + await _taskEvents.OnEndProcessTask(elementId, instance); } /// - public override async Task HandleTaskStart(ProcessChangeContext processChangeContext) + public override async Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill) { - await _taskEvents.OnStartProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance, processChangeContext.Prefill); + await _taskEvents.OnStartProcessTask(elementId, instance, prefill); } } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/DataTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/DataTask.cs index c167e16da..4821f61c5 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/DataTask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/DataTask.cs @@ -1,5 +1,4 @@ -using Altinn.App.Core.Interface; -using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.Elements { @@ -19,22 +18,22 @@ public DataTask(ITaskEvents taskEvents) } /// - public override async Task HandleTaskAbandon(ProcessChangeContext processChangeContext) + public override async Task HandleTaskAbandon(string elementId, Instance instance) { - await _taskEvents.OnAbandonProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance); + await _taskEvents.OnAbandonProcessTask(elementId, instance); } /// - public override async Task HandleTaskComplete(ProcessChangeContext processChangeContext) + public override async Task HandleTaskComplete(string elementId, Instance instance) { - await _taskEvents.OnEndProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance); + await _taskEvents.OnEndProcessTask(elementId, instance); } /// - public override async Task HandleTaskStart(ProcessChangeContext processChangeContext) + public override async Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill) { - await _taskEvents.OnStartProcessTask(processChangeContext.ElementToBeProcessed, - processChangeContext.Instance, processChangeContext.Prefill); + await _taskEvents.OnStartProcessTask(elementId, + instance, prefill); } } } \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ElementInfo.cs b/src/Altinn.App.Core/Internal/Process/Elements/ElementInfo.cs deleted file mode 100644 index 2d0c666a1..000000000 --- a/src/Altinn.App.Core/Internal/Process/Elements/ElementInfo.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Altinn.App.Core.Internal.Process.Elements -{ - /// - /// Represents information about an element in a BPMN description. - /// - public class ElementInfo - { - /// - /// The unique id of a specific element in the BPMN - /// - public string Id { get; set; } - - /// - /// The type of BPMN element - /// - public string ElementType { get; set; } - - /// - /// The name of the BPMN element - /// - public string Name { get; set; } - - /// - /// The altinn specific task type - /// - public string? AltinnTaskType { get; set; } - } -} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ExclusiveGateway.cs b/src/Altinn.App.Core/Internal/Process/Elements/ExclusiveGateway.cs index 0379ca56d..d108f28d4 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/ExclusiveGateway.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/ExclusiveGateway.cs @@ -13,6 +13,12 @@ public class ExclusiveGateway: ProcessElement /// [XmlAttribute("default")] public string? Default { get; set; } + + /// + /// + /// + [XmlElement("extensionElements")] + public ExtensionElements? ExtensionElements { get; set; } /// /// String representation of process element type diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ExtensionElements.cs b/src/Altinn.App.Core/Internal/Process/Elements/ExtensionElements.cs new file mode 100644 index 000000000..d5db11e70 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/ExtensionElements.cs @@ -0,0 +1,23 @@ +using System.Xml.Serialization; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; + +namespace Altinn.App.Core.Internal.Process.Elements +{ + /// + /// Class representing the extension elements + /// + public class ExtensionElements + { + /// + /// Gets or sets the altinn properties + /// + [XmlElement("taskExtension", Namespace = "http://altinn.no/process")] + public AltinnTaskExtension? TaskExtension { get; set; } + + /// + /// Gets or sets the altinn properties + /// + [XmlElement("gatewayExtension", Namespace = "http://altinn.no/process")] + public AltinnGatewayExtension? GatewayExtension { get; set; } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/FeedbackTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/FeedbackTask.cs index a4f37c312..c5ccdf41a 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/FeedbackTask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/FeedbackTask.cs @@ -1,5 +1,4 @@ -using Altinn.App.Core.Interface; -using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.Elements { @@ -19,21 +18,21 @@ public FeedbackTask(ITaskEvents taskEvents) } /// - public override async Task HandleTaskAbandon(ProcessChangeContext processChangeContext) + public override async Task HandleTaskAbandon(string elementId, Instance instance) { - await _taskEvents.OnAbandonProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance); + await _taskEvents.OnAbandonProcessTask(elementId, instance); } /// - public override async Task HandleTaskComplete(ProcessChangeContext processChangeContext) + public override async Task HandleTaskComplete(string elementId, Instance instance) { - await _taskEvents.OnEndProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance); + await _taskEvents.OnEndProcessTask(elementId, instance); } /// - public override async Task HandleTaskStart(ProcessChangeContext processChangeContext) + public override async Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill) { - await _taskEvents.OnStartProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance, processChangeContext.Prefill); + await _taskEvents.OnStartProcessTask(elementId, instance, prefill); } } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ITask.cs b/src/Altinn.App.Core/Internal/Process/Elements/ITask.cs index 813fa1946..ab07f273e 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/ITask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/ITask.cs @@ -1,4 +1,5 @@ using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.Elements { @@ -10,16 +11,16 @@ public interface ITask /// /// This operations triggers process logic needed to start the current task. The logic depend on the different types of task /// - Task HandleTaskStart(ProcessChangeContext processChangeContext); + Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill); /// /// This operatin triggers process logic need to complete a given task. The Logic depend on the different types of task. /// - Task HandleTaskComplete(ProcessChangeContext processChangeContext); + Task HandleTaskComplete(string elementId, Instance instance); /// /// This operatin triggers process logic need to abandon a Task without completing it /// - Task HandleTaskAbandon(ProcessChangeContext processChangeContext); + Task HandleTaskAbandon(string elementId, Instance instance); } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/NullTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/NullTask.cs index f4e151fe5..b8e79271f 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/NullTask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/NullTask.cs @@ -1,4 +1,5 @@ using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.Elements; @@ -8,19 +9,19 @@ namespace Altinn.App.Core.Internal.Process.Elements; public class NullTask: ITask { /// - public async Task HandleTaskStart(ProcessChangeContext processChangeContext) + public async Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill) { await Task.CompletedTask; } /// - public async Task HandleTaskComplete(ProcessChangeContext processChangeContext) + public async Task HandleTaskComplete(string elementId, Instance instance) { await Task.CompletedTask; } /// - public async Task HandleTaskAbandon(ProcessChangeContext processChangeContext) + public async Task HandleTaskAbandon(string elementId, Instance instance) { await Task.CompletedTask; } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs index e0b32d758..b80957b8e 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs @@ -9,10 +9,10 @@ namespace Altinn.App.Core.Internal.Process.Elements public class ProcessTask: ProcessElement { /// - /// Gets or sets the outgoing id of a task + /// Defines the extension elements /// - [XmlAttribute("tasktype", Namespace = "http://altinn.no")] - public string? TaskType { get; set; } + [XmlElement("extensionElements")] + public ExtensionElements? ExtensionElements { get; set; } /// /// String representation of process element type diff --git a/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs b/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs index 36646e73a..809b44b41 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs @@ -30,5 +30,11 @@ public class SequenceFlow /// [XmlAttribute("flowtype", Namespace = "http://altinn.no")] public string FlowType { get; set; } + + /// + /// Gets or sets the condition expression of a sequence flow + /// + [XmlElement("conditionExpression")] + public string? ConditionExpression { get; set; } } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/TaskBase.cs b/src/Altinn.App.Core/Internal/Process/Elements/TaskBase.cs index 77f3b528e..274a0a013 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/TaskBase.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/TaskBase.cs @@ -1,4 +1,5 @@ using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.Elements { @@ -10,16 +11,16 @@ public abstract class TaskBase: ITask /// /// hallooo asdf /// - public abstract Task HandleTaskComplete(ProcessChangeContext processChangeContext); + public abstract Task HandleTaskComplete(string elementId, Instance instance); /// /// Handle task start /// - public abstract Task HandleTaskStart(ProcessChangeContext processChangeContext); + public abstract Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill); /// /// Handle task abandon /// - public abstract Task HandleTaskAbandon(ProcessChangeContext processChangeContext); + public abstract Task HandleTaskAbandon(string elementId, Instance instance); } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/UserAction.cs b/src/Altinn.App.Core/Internal/Process/Elements/UserAction.cs new file mode 100644 index 000000000..2556ec2f0 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/UserAction.cs @@ -0,0 +1,31 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; + +namespace Altinn.App.Core.Internal.Process.Elements +{ + /// + /// Defines an altinn action for a task + /// + public class UserAction + { + /// + /// Gets or sets the ID of the action + /// + [JsonPropertyName("id")] + public required string Id { get; set; } + + /// + /// Gets or sets if the user is authorized to perform the action + /// + [JsonPropertyName("authorized")] + public bool Authorized { get; set; } + + /// + /// Gets or sets the type of action + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonPropertyName("type")] + public ActionType ActionType { get; set; } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs b/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs new file mode 100644 index 000000000..f105e2210 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs @@ -0,0 +1,169 @@ +using System.Text; +using System.Text.Json; +using Altinn.App.Core.Features; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Expressions; +using Altinn.App.Core.Models.Process; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process +{ + /// + /// Class implementing for evaluating expressions on flows connected to a gateway + /// + public class ExpressionsExclusiveGateway : IProcessExclusiveGateway + { + private readonly LayoutEvaluatorStateInitializer _layoutStateInit; + private readonly IAppResources _resources; + private readonly IAppMetadata _appMetadata; + private readonly IDataClient _dataClient; + private readonly IAppModel _appModel; + + /// + /// Constructor for + /// + /// Expressions state initalizer used to create context for expression evaluation + /// Service for fetching app resources + /// Service for fetching app model + /// Service for fetching app metadata + /// Service for interacting with Platform Storage + public ExpressionsExclusiveGateway( + LayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, + IAppResources resources, + IAppModel appModel, + IAppMetadata appMetadata, + IDataClient dataClient) + { + _layoutStateInit = layoutEvaluatorStateInitializer; + _resources = resources; + _appMetadata = appMetadata; + _dataClient = dataClient; + _appModel = appModel; + } + + /// + public string GatewayId { get; } = "AltinnExpressionsExclusiveGateway"; + + /// + public async Task> FilterAsync(List outgoingFlows, Instance instance, ProcessGatewayInformation processGatewayInformation) + { + var state = await GetLayoutEvaluatorState(instance, processGatewayInformation.Action, processGatewayInformation.DataTypeId); + + return outgoingFlows.Where(outgoingFlow => EvaluateSequenceFlow(state, outgoingFlow)).ToList(); + } + + private async Task GetLayoutEvaluatorState(Instance instance, string? action, string? dataTypeId) + { + var layoutSet = GetLayoutSet(instance); + var (checkedDataTypeId, dataType) = await GetDataType(instance, layoutSet, dataTypeId); + object data = new object(); + if (checkedDataTypeId != null && dataType != null) + { + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instance); + var dataGuid = GetDataId(instance, checkedDataTypeId); + Type dataElementType = dataType; + if (dataGuid != null) + { + data = await _dataClient.GetFormData(instanceIdentifier.InstanceGuid, dataElementType, instance.Org, instance.AppId.Split("/")[1], int.Parse(instance.InstanceOwner.PartyId), dataGuid.Value); + } + } + + var state = await _layoutStateInit.Init(instance, data, layoutSetId: layoutSet?.Id, gatewayAction: action); + return state; + } + + private static bool EvaluateSequenceFlow(LayoutEvaluatorState state, SequenceFlow sequenceFlow) + { + if (sequenceFlow.ConditionExpression != null) + { + var expression = GetExpressionFromCondition(sequenceFlow.ConditionExpression); + foreach (var componentContext in state.GetComponentContexts()) + { + var result = ExpressionEvaluator.EvaluateExpression(state, expression, componentContext); + if (result is bool boolResult && boolResult) + { + return true; + } + } + } + else + { + return true; + } + + return false; + } + + private static Expression GetExpressionFromCondition(string condition) + { + JsonSerializerOptions options = new() + { + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNameCaseInsensitive = true, + }; + Utf8JsonReader reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(condition)); + reader.Read(); + var expressionFromCondition = ExpressionConverter.ReadNotNull(ref reader, options); + return expressionFromCondition; + } + + private LayoutSet? GetLayoutSet(Instance instance) + { + string taskId = instance.Process.CurrentTask.ElementId; + JsonSerializerOptions options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + + string layoutSetsString = _resources.GetLayoutSets(); + LayoutSet? layoutSet = null; + if (!string.IsNullOrEmpty(layoutSetsString)) + { + LayoutSets? layoutSets = JsonSerializer.Deserialize(layoutSetsString, options); + layoutSet = layoutSets?.Sets?.Find(t => t.Tasks.Contains(taskId)); + } + + return layoutSet; + } + + //TODO: Find a better home for this method + private async Task<(string? DataTypeId, Type? DataTypeClassType)> GetDataType(Instance instance, LayoutSet? layoutSet, string? dataTypeId) + { + DataType? dataType; + if (dataTypeId != null) + { + dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.Find(d => d.Id == dataTypeId && d.AppLogic != null); + } + else if (layoutSet != null) + { + dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.Find(d => d.Id == layoutSet.DataType && d.AppLogic != null); + } + else + { + dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.Find(d => d.TaskId == instance.Process.CurrentTask.ElementId && d.AppLogic != null); + } + + if (dataType != null) + { + return (dataType.Id, _appModel.GetModelType(dataType.AppLogic.ClassRef)); + } + + return (null, null); + } + + private static Guid? GetDataId(Instance instance, string dataType) + { + string? dataId = instance.Data.Find(d => d.DataType == dataType)?.Id; + if (dataId != null) + { + return new Guid(dataId); + } + + return null; + } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/FlowHydration.cs b/src/Altinn.App.Core/Internal/Process/FlowHydration.cs deleted file mode 100644 index 122c94517..000000000 --- a/src/Altinn.App.Core/Internal/Process/FlowHydration.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Altinn.App.Core.Features; -using Altinn.App.Core.Internal.Process.Elements; -using Altinn.App.Core.Internal.Process.Elements.Base; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Internal.Process; - -/// -/// Class used to get next elements in the process based on the Process and custom implementations of to make gateway decisions. -/// -public class FlowHydration: IFlowHydration -{ - private readonly IProcessReader _processReader; - private readonly ExclusiveGatewayFactory _gatewayFactory; - - /// - /// Initialize a new instance of FlowHydration - /// - /// IProcessReader implementation used to read the process - /// ExclusiveGatewayFactory used to fetch gateway code to be executed - public FlowHydration(IProcessReader processReader, ExclusiveGatewayFactory gatewayFactory) - { - _processReader = processReader; - _gatewayFactory = gatewayFactory; - } - - /// - public async Task> NextFollowAndFilterGateways(Instance instance, string? currentElement, bool followDefaults = true) - { - List directFlowTargets = _processReader.GetNextElements(currentElement); - return await NextFollowAndFilterGateways(instance, directFlowTargets!, followDefaults); - } - - /// - public async Task> NextFollowAndFilterGateways(Instance instance, List originNextElements, bool followDefaults = true) - { - List filteredNext = new List(); - foreach (var directFlowTarget in originNextElements) - { - if (directFlowTarget == null) - { - continue; - } - if (!IsGateway(directFlowTarget)) - { - filteredNext.Add(directFlowTarget); - continue; - } - - var gateway = (ExclusiveGateway)directFlowTarget; - IProcessExclusiveGateway? gatewayFilter = _gatewayFactory.GetProcessExclusiveGateway(directFlowTarget.Id); - List outgoingFlows = _processReader.GetOutgoingSequenceFlows(directFlowTarget); - List filteredList; - if (gatewayFilter == null) - { - filteredList = outgoingFlows; - } - else - { - filteredList = await gatewayFilter.FilterAsync(outgoingFlows, instance); - } - - var defaultSequenceFlow = filteredList.Find(s => s.Id == gateway.Default); - if (followDefaults && defaultSequenceFlow != null) - { - var defaultTarget = _processReader.GetFlowElement(defaultSequenceFlow.TargetRef); - filteredNext.AddRange(await NextFollowAndFilterGateways(instance, new List { defaultTarget })); - } - else - { - var filteredTargets= filteredList.Select(e => _processReader.GetFlowElement(e.TargetRef)).ToList(); - filteredNext.AddRange(await NextFollowAndFilterGateways(instance, filteredTargets)); - } - } - - return filteredNext; - } - - private static bool IsGateway(ProcessElement processElement) - { - return processElement is ExclusiveGateway; - } -} diff --git a/src/Altinn.App.Core/Internal/Process/IFlowHydration.cs b/src/Altinn.App.Core/Internal/Process/IFlowHydration.cs deleted file mode 100644 index e5218dce1..000000000 --- a/src/Altinn.App.Core/Internal/Process/IFlowHydration.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Altinn.App.Core.Features; -using Altinn.App.Core.Internal.Process.Elements.Base; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Internal.Process; - -/// -/// Defines method needed for filtering process flows based on application configuration -/// -public interface IFlowHydration -{ - /// - /// Checks next elements of current for gateways and apply custom gateway decisions based on implementations - /// - /// Instance data - /// Current process element id - /// Should follow default path out of gateway if set - /// Filtered list of next elements - public Task> NextFollowAndFilterGateways(Instance instance, string? currentElement, bool followDefaults = true); - - /// - /// Takes a list of flows checks for gateways and apply custom gateway decisions based on implementations - /// - /// Instance data - /// Original list of next elements - /// /// Should follow default path out of gateway if set - /// Filtered list of next elements - public Task> NextFollowAndFilterGateways(Instance instance, List originNextElements, bool followDefaults = true); -} diff --git a/src/Altinn.App.Core/Internal/Process/IProcessClient.cs b/src/Altinn.App.Core/Internal/Process/IProcessClient.cs new file mode 100644 index 000000000..d204ffcf5 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/IProcessClient.cs @@ -0,0 +1,21 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process +{ + /// + /// Process service that encapsulate reading of the BPMN process definition. + /// + public interface IProcessClient + { + /// + /// Returns a stream that contains the process definition. + /// + /// the stream + Stream GetProcessDefinition(); + + /// + /// Gets the instance process events related to the instance matching the instance id. + /// + Task GetProcessHistory(string instanceGuid, string instanceOwnerPartyId); + } +} diff --git a/src/Altinn.App.Core/Internal/Process/IProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/IProcessEngine.cs new file mode 100644 index 000000000..bb62df68a --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/IProcessEngine.cs @@ -0,0 +1,29 @@ +using Altinn.App.Core.Models.Process; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process +{ + /// + /// Process engine interface that defines the Altinn App process engine + /// + public interface IProcessEngine + { + /// + /// Method to start a new process + /// + Task StartProcess(ProcessStartRequest processStartRequest); + + /// + /// Method to move process to next task/event + /// + Task Next(ProcessNextRequest request); + + /// + /// Update Instance and rerun instance events + /// + /// + /// + /// + Task UpdateInstanceAndRerunEvents(ProcessStartRequest startRequest, List? events); + } +} diff --git a/src/Altinn.App.Core/Internal/Process/IProcessEventDispatcher.cs b/src/Altinn.App.Core/Internal/Process/IProcessEventDispatcher.cs new file mode 100644 index 000000000..119fedf29 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/IProcessEventDispatcher.cs @@ -0,0 +1,23 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process; + +/// +/// Interface for dispatching events that occur during a process +/// +public interface IProcessEventDispatcher +{ + /// + /// Updates the instance process in storage and dispatches instance events + /// + /// The instance with updated process + /// Prefill data + /// Events that should be dispatched + /// Instance from storage after update + Task UpdateProcessAndDispatchEvents(Instance instance, Dictionary? prefill, List? events); + /// + /// Dispatch events for instance to the events system if AppSettings.RegisterEventsWithEventsComponent is true + /// + /// The instance to dispatch events for + Task RegisterEventWithEventsComponent(Instance instance); +} diff --git a/src/Altinn.App.Core/Internal/Process/IProcessNavigator.cs b/src/Altinn.App.Core/Internal/Process/IProcessNavigator.cs new file mode 100644 index 000000000..75553bdbf --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/IProcessNavigator.cs @@ -0,0 +1,20 @@ +using Altinn.App.Core.Internal.Process.Elements.Base; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process +{ + /// + /// Interface used to descipt the process navigator + /// + public interface IProcessNavigator + { + /// + /// Get the next task in the process from the current element based on the action and datadriven gateway decisions + /// + /// Instance data + /// Current process element id + /// Action performed + /// The next process task + public Task GetNextTask(Instance instance, string currentElement, string? action); + } +} diff --git a/src/Altinn.App.Core/Internal/Process/IProcessReader.cs b/src/Altinn.App.Core/Internal/Process/IProcessReader.cs index 0c5260a76..492688071 100644 --- a/src/Altinn.App.Core/Internal/Process/IProcessReader.cs +++ b/src/Altinn.App.Core/Internal/Process/IProcessReader.cs @@ -8,7 +8,6 @@ namespace Altinn.App.Core.Internal.Process; /// public interface IProcessReader { - /// /// Get all defined StartEvents in the process /// @@ -84,39 +83,26 @@ public interface IProcessReader /// public List GetSequenceFlows(); + /// + /// Get SequenceFlows out of the bpmn element + /// + /// Element to get the outgoing sequenceflows from + /// Outgoing sequence flows + public List GetOutgoingSequenceFlows(ProcessElement? flowElement); /// /// Get ids of all SequenceFlows defined in the process /// /// public List GetSequenceFlowIds(); - + /// /// Find all possible next elements from current element /// /// Current process element id /// public List GetNextElements(string? currentElementId); - - /// - /// Find ids of all possible next elements from current element - /// - /// Current ProcessElement Id - /// - public List GetNextElementIds(string? currentElement); - - /// - /// Get SequenceFlows out of the bpmn element - /// - /// Element to get the outgoing sequenceflows from - /// Outgoing sequence flows - public List GetOutgoingSequenceFlows(ProcessElement? flowElement); - /// - /// Returns a list of sequence flow to be followed between current step and next element - /// - public List GetSequenceFlowsBetween(string? currentStepId, string? nextElementId); - /// /// Returns StartEvent, Task or EndEvent with given Id, null if element not found /// @@ -125,10 +111,8 @@ public interface IProcessReader public ProcessElement? GetFlowElement(string? elementId); /// - /// Retuns ElementInfo for StartEvent, Task or EndEvent with given Id, null if element not found + /// Returns all available ProcessElements /// - /// Id of element to look for - /// or null - public ElementInfo? GetElementInfo(string? elementId); - + /// + public List GetAllFlowElements(); } diff --git a/src/Altinn.App.Core/Internal/Process/ITaskEvents.cs b/src/Altinn.App.Core/Internal/Process/ITaskEvents.cs new file mode 100644 index 000000000..87c8cbb6d --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ITaskEvents.cs @@ -0,0 +1,34 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process; + +/// +/// Interface for implementing a receiver handling task process events. +/// +public interface ITaskEvents +{ + /// + /// Callback to app after task has been started. + /// + /// task id of task started + /// Instance data + /// Prefill data + /// + public Task OnStartProcessTask(string taskId, Instance instance, Dictionary prefill); + + /// + /// Is called after the process task is ended. Method can update instance and data element metadata. + /// + /// task id of task ended + /// Instance data + /// + public Task OnEndProcessTask(string endEvent, Instance instance); + + /// + /// Is called after the process task is abonded. Method can update instance and data element metadata. + /// + /// task id of task to abandon + /// Instance data + public Task OnAbandonProcessTask(string taskId, Instance instance); + +} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessChangeHandler.cs b/src/Altinn.App.Core/Internal/Process/ProcessChangeHandler.cs deleted file mode 100644 index ab3442956..000000000 --- a/src/Altinn.App.Core/Internal/Process/ProcessChangeHandler.cs +++ /dev/null @@ -1,449 +0,0 @@ -using System.Security.Claims; -using Altinn.App.Core.Configuration; -using Altinn.App.Core.Extensions; -using Altinn.App.Core.Features.Validation; -using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; -using Altinn.App.Core.Internal.Process.Elements; -using Altinn.App.Core.Models; -using Altinn.App.Core.Models.Validation; -using Altinn.Platform.Profile.Models; -using Altinn.Platform.Storage.Interface.Enums; -using Altinn.Platform.Storage.Interface.Models; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Altinn.App.Core.Internal.Process -{ - /// - /// Handler that implements needed logic related to different process changes. Identifies the correct types of tasks and trigger the different task and event - /// - /// While ProcessEngine.cs only understand standard BPMN process this handler fully understand the Altinn App context - /// - public class ProcessChangeHandler : IProcessChangeHandler - { - private readonly IInstance _instanceClient; - private readonly IProcess _processService; - private readonly IProcessReader _processReader; - private readonly ILogger _logger; - private readonly IValidation _validationService; - private readonly IEvents _eventsService; - private readonly IProfile _profileClient; - private readonly AppSettings _appSettings; - private readonly IAppEvents _appEvents; - private readonly ITaskEvents _taskEvents; - - /// - /// Altinn App specific process change handler - /// - public ProcessChangeHandler( - ILogger logger, - IProcess processService, - IProcessReader processReader, - IInstance instanceClient, - IValidation validationService, - IEvents eventsService, - IProfile profileClient, - IOptions appSettings, - IAppEvents appEvents, - ITaskEvents taskEvents) - { - _logger = logger; - _processService = processService; - _instanceClient = instanceClient; - _processReader = processReader; - _validationService = validationService; - _eventsService = eventsService; - _profileClient = profileClient; - _appSettings = appSettings.Value; - _appEvents = appEvents; - _taskEvents = taskEvents; - } - - /// - public async Task HandleMoveToNext(ProcessChangeContext processChange) - { - processChange.ProcessStateChange = await ProcessNext(processChange.Instance, processChange.RequestedProcessElementId, processChange.User); - if (processChange.ProcessStateChange != null) - { - processChange.Instance = await UpdateProcessAndDispatchEvents(processChange); - - await RegisterEventWithEventsComponent(processChange.Instance); - } - - return processChange; - } - - /// - public async Task HandleStart(ProcessChangeContext processChange) - { - // start process - ProcessStateChange startChange = await ProcessStart(processChange.Instance, processChange.ProcessFlowElements[0], processChange.User); - InstanceEvent startEvent = CopyInstanceEventValue(startChange.Events.First()); - - ProcessStateChange nextChange = await ProcessNext(processChange.Instance, processChange.ProcessFlowElements[1], processChange.User); - InstanceEvent goToNextEvent = CopyInstanceEventValue(nextChange.Events.First()); - - ProcessStateChange processStateChange = new ProcessStateChange - { - OldProcessState = startChange.OldProcessState, - NewProcessState = nextChange.NewProcessState, - Events = new List { startEvent, goToNextEvent } - }; - processChange.ProcessStateChange = processStateChange; - - if (!processChange.DontUpdateProcessAndDispatchEvents) - { - processChange.Instance = await UpdateProcessAndDispatchEvents(processChange); - } - - return processChange; - } - - /// - public async Task HandleStartTask(ProcessChangeContext processChange) - { - processChange.Instance = await UpdateProcessAndDispatchEvents(processChange); - return processChange; - } - - /// - public async Task CanTaskBeEnded(ProcessChangeContext processChange) - { - List validationIssues = new List(); - - bool canEndTask; - - if (processChange.Instance.Process?.CurrentTask?.Validated == null || !processChange.Instance.Process.CurrentTask.Validated.CanCompleteTask) - { - validationIssues = await _validationService.ValidateAndUpdateProcess(processChange.Instance, processChange.Instance.Process.CurrentTask?.ElementId); - - canEndTask = await ProcessHelper.CanEndProcessTask(processChange.Instance, validationIssues); - } - else - { - canEndTask = await ProcessHelper.CanEndProcessTask(processChange.Instance, validationIssues); - } - - return canEndTask; - } - - /// - /// Identify the correct task implementation - /// - /// - private ITask GetProcessTask(string? altinnTaskType) - { - if (string.IsNullOrEmpty(altinnTaskType)) - { - return new NullTask(); - } - - ITask task = new DataTask(_taskEvents); - if (altinnTaskType.Equals("confirmation")) - { - task = new ConfirmationTask(_taskEvents); - } - else if (altinnTaskType.Equals("feedback")) - { - task = new FeedbackTask(_taskEvents); - } - - return task; - } - - /// - /// This - /// - private async Task UpdateProcessAndDispatchEvents(ProcessChangeContext processChangeContext) - { - await HandleProcessChanges(processChangeContext); - - // need to update the instance process and then the instance in case appbase has changed it, e.g. endEvent sets status.archived - Instance updatedInstance = await _instanceClient.UpdateProcess(processChangeContext.Instance); - await _processService.DispatchProcessEventsToStorage(updatedInstance, processChangeContext.ProcessStateChange.Events); - - // remember to get the instance anew since AppBase can have updated a data element or stored something in the database. - updatedInstance = await _instanceClient.GetInstance(updatedInstance); - - return updatedInstance; - } - - /// - /// Will for each process change trigger relevant Process Elements to perform the relevant change actions. - /// - /// Each implementation - /// - internal async Task HandleProcessChanges(ProcessChangeContext processChangeContext) - { - foreach (InstanceEvent processEvent in processChangeContext.ProcessStateChange.Events) - { - if (Enum.TryParse(processEvent.EventType, true, out InstanceEventType eventType)) - { - processChangeContext.ElementToBeProcessed = processEvent.ProcessInfo?.CurrentTask?.ElementId; - ITask task = GetProcessTask(processEvent.ProcessInfo?.CurrentTask?.AltinnTaskType); - switch (eventType) - { - case InstanceEventType.process_StartEvent: - break; - case InstanceEventType.process_StartTask: - await task.HandleTaskStart(processChangeContext); - break; - case InstanceEventType.process_EndTask: - await task.HandleTaskComplete(processChangeContext); - break; - case InstanceEventType.process_AbandonTask: - await task.HandleTaskAbandon(processChangeContext); - await _instanceClient.UpdateProcess(processChangeContext.Instance); - break; - case InstanceEventType.process_EndEvent: - processChangeContext.ElementToBeProcessed = processEvent.ProcessInfo?.EndEvent; - await _appEvents.OnEndAppEvent(processEvent.ProcessInfo?.EndEvent, processChangeContext.Instance); - break; - } - } - } - } - - /// - /// Does not save process. Instance is updated. - /// - private async Task ProcessStart(Instance instance, string startEvent, ClaimsPrincipal user) - { - if (instance.Process == null) - { - DateTime now = DateTime.UtcNow; - - ProcessState startState = new ProcessState - { - Started = now, - StartEvent = startEvent, - CurrentTask = new ProcessElementInfo { Flow = 1 } - }; - - instance.Process = startState; - - List events = new List - { - await GenerateProcessChangeEvent(InstanceEventType.process_StartEvent.ToString(), instance, now, user), - }; - - return new ProcessStateChange - { - OldProcessState = null!, - NewProcessState = startState, - Events = events, - }; - } - - return null; - } - - private async Task GenerateProcessChangeEvent(string eventType, Instance instance, DateTime now, ClaimsPrincipal user) - { - int? userId = user.GetUserIdAsInt(); - InstanceEvent instanceEvent = new InstanceEvent - { - InstanceId = instance.Id, - InstanceOwnerPartyId = instance.InstanceOwner.PartyId, - EventType = eventType, - Created = now, - User = new PlatformUser - { - UserId = userId, - AuthenticationLevel = user.GetAuthenticationLevel(), - OrgId = user.GetOrg() - }, - ProcessInfo = instance.Process, - }; - - if (string.IsNullOrEmpty(instanceEvent.User.OrgId) && userId != null) - { - UserProfile up = await _profileClient.GetUserProfile((int)userId); - instanceEvent.User.NationalIdentityNumber = up.Party.SSN; - } - - return instanceEvent; - } - - private static InstanceEvent CopyInstanceEventValue(InstanceEvent e) - { - return new InstanceEvent - { - Created = e.Created, - DataId = e.DataId, - EventType = e.EventType, - Id = e.Id, - InstanceId = e.InstanceId, - InstanceOwnerPartyId = e.InstanceOwnerPartyId, - ProcessInfo = new ProcessState - { - Started = e.ProcessInfo?.Started, - CurrentTask = new ProcessElementInfo - { - Flow = e.ProcessInfo?.CurrentTask.Flow, - AltinnTaskType = e.ProcessInfo?.CurrentTask.AltinnTaskType, - ElementId = e.ProcessInfo?.CurrentTask.ElementId, - Name = e.ProcessInfo?.CurrentTask.Name, - Started = e.ProcessInfo?.CurrentTask.Started, - Ended = e.ProcessInfo?.CurrentTask.Ended, - Validated = new ValidationStatus - { - CanCompleteTask = e.ProcessInfo?.CurrentTask?.Validated?.CanCompleteTask ?? false, - Timestamp = e.ProcessInfo?.CurrentTask?.Validated?.Timestamp - } - }, - - StartEvent = e.ProcessInfo?.StartEvent - }, - User = new PlatformUser - { - AuthenticationLevel = e.User.AuthenticationLevel, - EndUserSystemId = e.User.EndUserSystemId, - OrgId = e.User.OrgId, - UserId = e.User.UserId, - NationalIdentityNumber = e.User?.NationalIdentityNumber - } - }; - } - - /// - /// Moves instance's process to nextElement id. Returns the instance together with process events. - /// - public async Task ProcessNext(Instance instance, string? nextElementId, ClaimsPrincipal userContext) - { - if (instance.Process != null) - { - ProcessStateChange result = new ProcessStateChange - { - OldProcessState = new ProcessState() - { - Started = instance.Process.Started, - CurrentTask = instance.Process.CurrentTask, - StartEvent = instance.Process.StartEvent - } - }; - - result.Events = await MoveProcessToNext(instance, nextElementId, userContext); - result.NewProcessState = instance.Process; - return result; - } - - return null; - } - - /// - /// Assumes that nextElementId is a valid task/state - /// - private async Task> MoveProcessToNext( - Instance instance, - string? nextElementId, - ClaimsPrincipal user) - { - List events = new List(); - - ProcessState previousState = Copy(instance.Process); - ProcessState currentState = instance.Process; - string? previousElementId = currentState.CurrentTask?.ElementId; - - ElementInfo? nextElementInfo = _processReader.GetElementInfo(nextElementId); - List flows = _processReader.GetSequenceFlowsBetween(previousElementId, nextElementId); - ProcessSequenceFlowType sequenceFlowType = ProcessHelper.GetSequenceFlowType(flows); - DateTime now = DateTime.UtcNow; - bool previousIsProcessTask = _processReader.IsProcessTask(previousElementId); - // ending previous element if task - if (previousIsProcessTask && sequenceFlowType.Equals(ProcessSequenceFlowType.CompleteCurrentMoveToNext)) - { - instance.Process = previousState; - events.Add(await GenerateProcessChangeEvent(InstanceEventType.process_EndTask.ToString(), instance, now, user)); - instance.Process = currentState; - } - else if (previousIsProcessTask) - { - instance.Process = previousState; - events.Add(await GenerateProcessChangeEvent(InstanceEventType.process_AbandonTask.ToString(), instance, now, user)); - instance.Process = currentState; - } - - // ending process if next element is end event - if (_processReader.IsEndEvent(nextElementId)) - { - currentState.CurrentTask = null; - currentState.Ended = now; - currentState.EndEvent = nextElementId; - - events.Add(await GenerateProcessChangeEvent(InstanceEventType.process_EndEvent.ToString(), instance, now, user)); - - // add submit event (to support Altinn2 SBL) - events.Add(await GenerateProcessChangeEvent(InstanceEventType.Submited.ToString(), instance, now, user)); - } - else if (_processReader.IsProcessTask(nextElementId)) - { - currentState.CurrentTask = new ProcessElementInfo - { - Flow = currentState.CurrentTask.Flow + 1, - ElementId = nextElementId, - Name = nextElementInfo?.Name, - Started = now, - AltinnTaskType = nextElementInfo?.AltinnTaskType, - Validated = null, - FlowType = sequenceFlowType.ToString(), - }; - - events.Add(await GenerateProcessChangeEvent(InstanceEventType.process_StartTask.ToString(), instance, now, user)); - } - - // current state points to the instance's process object. The following statement is unnecessary, but clarifies logic. - instance.Process = currentState; - - return events; - } - - private async Task RegisterEventWithEventsComponent(Instance instance) - { - if (_appSettings.RegisterEventsWithEventsComponent) - { - try - { - if (!string.IsNullOrWhiteSpace(instance.Process.CurrentTask?.ElementId)) - { - await _eventsService.AddEvent($"app.instance.process.movedTo.{instance.Process.CurrentTask.ElementId}", instance); - } - else if (instance.Process.EndEvent != null) - { - await _eventsService.AddEvent("app.instance.process.completed", instance); - } - } - catch (Exception exception) - { - _logger.LogWarning(exception, "Exception when sending event with the Events component"); - } - } - } - - private static ProcessState Copy(ProcessState original) - { - ProcessState processState = new ProcessState(); - - if (original.CurrentTask != null) - { - processState.CurrentTask = new ProcessElementInfo(); - processState.CurrentTask.FlowType = original.CurrentTask.FlowType; - processState.CurrentTask.Name = original.CurrentTask.Name; - processState.CurrentTask.Validated = original.CurrentTask.Validated; - processState.CurrentTask.AltinnTaskType = original.CurrentTask.AltinnTaskType; - processState.CurrentTask.Flow = original.CurrentTask.Flow; - processState.CurrentTask.ElementId = original.CurrentTask.ElementId; - processState.CurrentTask.Started = original.CurrentTask.Started; - processState.CurrentTask.Ended = original.CurrentTask.Ended; - } - - processState.EndEvent = original.EndEvent; - processState.Started = original.Started; - processState.Ended = original.Ended; - processState.StartEvent = original.StartEvent; - - return processState; - } - } -} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 2f08f41ca..49e7b8d06 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -1,138 +1,323 @@ +using System.Security.Claims; +using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features.Action; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.Base; -using Altinn.App.Core.Models; -using Microsoft.IdentityModel.Tokens; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Models.Process; +using Altinn.App.Core.Models.UserAction; +using Altinn.Platform.Profile.Models; +using Altinn.Platform.Storage.Interface.Enums; +using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process +namespace Altinn.App.Core.Internal.Process; + +/// +/// Default implementation of the +/// +public class ProcessEngine : IProcessEngine { + private readonly IProcessReader _processReader; + private readonly IProfileClient _profileClient; + private readonly IProcessNavigator _processNavigator; + private readonly IProcessEventDispatcher _processEventDispatcher; + private readonly UserActionService _userActionService; + /// - /// The process engine is responsible for all BMPN related functionality - /// - /// It will call processChange handler that is responsible - /// for the business logic happening for any process change. + /// Initializes a new instance of the class /// - public class ProcessEngine : IProcessEngine + /// Process reader service + /// The profile service + /// The process navigator + /// The process event dispatcher + /// The action handler factory + public ProcessEngine( + IProcessReader processReader, + IProfileClient profileClient, + IProcessNavigator processNavigator, + IProcessEventDispatcher processEventDispatcher, + UserActionService userActionService) + { + _processReader = processReader; + _profileClient = profileClient; + _processNavigator = processNavigator; + _processEventDispatcher = processEventDispatcher; + _userActionService = userActionService; + } + + /// + public async Task StartProcess(ProcessStartRequest processStartRequest) { - private readonly IProcessChangeHandler _processChangeHandler; - - private readonly IProcessReader _processReader; - private readonly IFlowHydration _flowHydration; - - /// - /// Initializes a new instance of the class. - /// - public ProcessEngine( - IProcessChangeHandler processChangeHandler, - IProcessReader processReader, - IFlowHydration flowHydration) - { - _processChangeHandler = processChangeHandler; - _processReader = processReader; - _flowHydration = flowHydration; + if (processStartRequest.Instance.Process != null) + { + return new ProcessChangeResult() + { + Success = false, + ErrorMessage = "Process is already started. Use next.", + ErrorType = ProcessErrorType.Conflict + }; + } + + string? validStartElement = ProcessHelper.GetValidStartEventOrError(processStartRequest.StartEventId, _processReader.GetStartEventIds(), out ProcessError? startEventError); + if (startEventError != null) + { + return new ProcessChangeResult() + { + Success = false, + ErrorMessage = "No matching startevent", + ErrorType = ProcessErrorType.Conflict + }; + } + + // start process + ProcessStateChange? startChange = await ProcessStart(processStartRequest.Instance, validStartElement!, processStartRequest.User); + InstanceEvent? startEvent = startChange?.Events?[0].CopyValues(); + ProcessStateChange? nextChange = await ProcessNext(processStartRequest.Instance, processStartRequest.User); + InstanceEvent? goToNextEvent = nextChange?.Events?[0].CopyValues(); + List events = new List(); + if (startEvent is not null) + { + events.Add(startEvent); + } + + if (goToNextEvent is not null) + { + events.Add(goToNextEvent); + } + + ProcessStateChange processStateChange = new ProcessStateChange + { + OldProcessState = startChange?.OldProcessState, + NewProcessState = nextChange?.NewProcessState, + Events = events + }; + + if (!processStartRequest.Dryrun) + { + await _processEventDispatcher.UpdateProcessAndDispatchEvents(processStartRequest.Instance, processStartRequest.Prefill, events); } - /// - /// Move process to next element in process - /// - public async Task Next(ProcessChangeContext processChange) + return new ProcessChangeResult() { - string? currentElementId = processChange.Instance.Process.CurrentTask?.ElementId; + Success = true, + ProcessStateChange = processStateChange + }; + } + + /// + public async Task Next(ProcessNextRequest request) + { + var instance = request.Instance; + string? currentElementId = instance.Process?.CurrentTask?.ElementId; - if (currentElementId == null) + if (currentElementId == null) + { + return new ProcessChangeResult() { - processChange.ProcessMessages = new List(); - processChange.ProcessMessages.Add(new ProcessChangeInfo() { Message = $"Instance does not have current task information!", Type = "Conflict" }); - processChange.FailedProcessChange = true; - return processChange; - } + Success = false, + ErrorMessage = $"Instance does not have current task information!", + ErrorType = ProcessErrorType.Conflict + }; + } - if (currentElementId.Equals(processChange.RequestedProcessElementId)) + int? userId = request.User.GetUserIdAsInt(); + if (userId == null) + { + return new ProcessChangeResult() { - processChange.ProcessMessages = new List(); - processChange.ProcessMessages.Add(new ProcessChangeInfo() { Message = $"Requested process element {processChange.RequestedProcessElementId} is same as instance's current task. Cannot change process.", Type = "Conflict" }); - processChange.FailedProcessChange = true; - return processChange; - } + Success = false, + ErrorMessage = $"User does not have a valid user id!", + ErrorType = ProcessErrorType.Conflict + }; + } - // Find next valid element. Later this will be dynamic - List possibleNextElements = await _flowHydration.NextFollowAndFilterGateways(processChange.Instance, currentElementId, processChange.RequestedProcessElementId.IsNullOrEmpty()); - processChange.RequestedProcessElementId = ProcessHelper.GetValidNextElementOrError(processChange.RequestedProcessElementId, possibleNextElements.Select(e => e.Id).ToList(),out ProcessError? nextElementError); - if (nextElementError != null) + var actionHandler = _userActionService.GetActionHandler(request.Action); + var actionResult = actionHandler is null ? UserActionResult.SuccessResult() : await actionHandler.HandleAction(new UserActionContext(request.Instance, userId.Value)); + + if (!actionResult.Success) + { + return new ProcessChangeResult() { - processChange.ProcessMessages = new List(); - processChange.ProcessMessages.Add(new ProcessChangeInfo() { Message = nextElementError.Text, Type = "Conflict" }); - processChange.FailedProcessChange = true; - return processChange; - } + Success = false, + ErrorMessage = $"Action handler for action {request.Action} failed!", + ErrorType = ProcessErrorType.Internal + }; + } + + var nextResult = await HandleMoveToNext(instance, request.User, request.Action); + + return new ProcessChangeResult() + { + Success = true, + ProcessStateChange = nextResult + }; + } - List flows = _processReader.GetSequenceFlowsBetween(currentElementId, processChange.RequestedProcessElementId); - processChange.ProcessSequenceFlowType = ProcessHelper.GetSequenceFlowType(flows); + /// + public async Task UpdateInstanceAndRerunEvents(ProcessStartRequest startRequest, List? events) + { + return await _processEventDispatcher.UpdateProcessAndDispatchEvents(startRequest.Instance, startRequest.Prefill, events); + } + + /// + /// Does not save process. Instance object is updated. + /// + private async Task ProcessStart(Instance instance, string startEvent, ClaimsPrincipal user) + { + if (instance.Process == null) + { + DateTime now = DateTime.UtcNow; - if (processChange.ProcessSequenceFlowType.Equals(ProcessSequenceFlowType.CompleteCurrentMoveToNext) && await _processChangeHandler.CanTaskBeEnded(processChange)) + ProcessState startState = new ProcessState { - return await _processChangeHandler.HandleMoveToNext(processChange); - } + Started = now, + StartEvent = startEvent, + CurrentTask = new ProcessElementInfo { Flow = 1, ElementId = startEvent } + }; + + instance.Process = startState; - if (processChange.ProcessSequenceFlowType.Equals(ProcessSequenceFlowType.AbandonCurrentReturnToNext)) + List events = new List { - return await _processChangeHandler.HandleMoveToNext(processChange); - } + await GenerateProcessChangeEvent(InstanceEventType.process_StartEvent.ToString(), instance, now, user), + }; - processChange.FailedProcessChange = true; - processChange.ProcessMessages = new List(); - processChange.ProcessMessages.Add(new ProcessChangeInfo() { Message = $"Cannot complete/close current task {currentElementId}. The data element(s) assigned to the task are not valid!", Type = "conflict" }); - return processChange; + return new ProcessStateChange + { + OldProcessState = null!, + NewProcessState = startState, + Events = events, + }; } - /// - /// Start application process and goes to first valid Task - /// - public async Task StartProcess(ProcessChangeContext processChange) + return null; + } + + /// + /// Moves instance's process to nextElement id. Returns the instance together with process events. + /// + private async Task ProcessNext(Instance instance, ClaimsPrincipal userContext, string? action = null) + { + if (instance.Process != null) { - if (processChange.Instance.Process != null) + ProcessStateChange result = new ProcessStateChange { - processChange.ProcessMessages = new List(); - processChange.ProcessMessages.Add(new ProcessChangeInfo() { Message = "Process is already started. Use next.", Type = "Conflict" }); - processChange.FailedProcessChange = true; - return processChange; - } + OldProcessState = new ProcessState() + { + Started = instance.Process.Started, + CurrentTask = instance.Process.CurrentTask, + StartEvent = instance.Process.StartEvent + } + }; + + result.Events = await MoveProcessToNext(instance, userContext, action); + result.NewProcessState = instance.Process; + return result; + } + + return null; + } + + private async Task> MoveProcessToNext( + Instance instance, + ClaimsPrincipal user, + string? action = null) + { + List events = new List(); - string? validStartElement = ProcessHelper.GetValidStartEventOrError(processChange.RequestedProcessElementId, _processReader.GetStartEventIds(),out ProcessError? startEventError); - if (startEventError != null) + ProcessState previousState = instance.Process.Copy(); + ProcessState currentState = instance.Process; + string? previousElementId = currentState.CurrentTask?.ElementId; + + ProcessElement? nextElement = await _processNavigator.GetNextTask(instance, instance.Process.CurrentTask.ElementId, action); + DateTime now = DateTime.UtcNow; + // ending previous element if task + if (_processReader.IsProcessTask(previousElementId)) + { + instance.Process = previousState; + string eventType = InstanceEventType.process_EndTask.ToString(); + if (action is "reject") { - processChange.ProcessMessages = new List(); - processChange.ProcessMessages.Add(new ProcessChangeInfo() { Message = "No matching startevent", Type = "Conflict" }); - processChange.FailedProcessChange = true; - return processChange; + eventType = InstanceEventType.process_AbandonTask.ToString(); } - processChange.ProcessFlowElements = new List(); - processChange.ProcessFlowElements.Add(validStartElement!); + events.Add(await GenerateProcessChangeEvent(eventType, instance, now, user)); + instance.Process = currentState; + } + + // ending process if next element is end event + if (_processReader.IsEndEvent(nextElement?.Id)) + { + currentState.CurrentTask = null; + currentState.Ended = now; + currentState.EndEvent = nextElement!.Id; - // find next task - List possibleNextElements = (await _flowHydration.NextFollowAndFilterGateways(processChange.Instance, validStartElement)); - string? nextValidElement = ProcessHelper.GetValidNextElementOrError(null, possibleNextElements.Select(e => e.Id).ToList(),out ProcessError? nextElementError); - if (nextElementError != null) + events.Add(await GenerateProcessChangeEvent(InstanceEventType.process_EndEvent.ToString(), instance, now, user)); + + // add submit event (to support Altinn2 SBL) + events.Add(await GenerateProcessChangeEvent(InstanceEventType.Submited.ToString(), instance, now, user)); + } + else if (_processReader.IsProcessTask(nextElement?.Id)) + { + var task = nextElement as ProcessTask; + currentState.CurrentTask = new ProcessElementInfo { - processChange.ProcessMessages = new List(); - processChange.ProcessMessages.Add(new ProcessChangeInfo() { Message = $"Unable to goto next element due to {nextElementError.Code}-{nextElementError.Text}", Type = "Conflict" }); - processChange.FailedProcessChange = true; - return processChange; - } + Flow = currentState.CurrentTask?.Flow + 1, + ElementId = nextElement!.Id, + Name = nextElement!.Name, + Started = now, + AltinnTaskType = task?.ExtensionElements?.TaskExtension?.TaskType, + FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), + Validated = null, + }; + + events.Add(await GenerateProcessChangeEvent(InstanceEventType.process_StartTask.ToString(), instance, now, user)); + } - processChange.ProcessFlowElements.Add(nextValidElement!); + // current state points to the instance's process object. The following statement is unnecessary, but clarifies logic. + instance.Process = currentState; + + return events; + } - return await _processChangeHandler.HandleStart(processChange); + private async Task GenerateProcessChangeEvent(string eventType, Instance instance, DateTime now, ClaimsPrincipal user) + { + int? userId = user.GetUserIdAsInt(); + InstanceEvent instanceEvent = new InstanceEvent + { + InstanceId = instance.Id, + InstanceOwnerPartyId = instance.InstanceOwner.PartyId, + EventType = eventType, + Created = now, + User = new PlatformUser + { + UserId = userId, + AuthenticationLevel = user.GetAuthenticationLevel(), + OrgId = user.GetOrg() + }, + ProcessInfo = instance.Process, + }; + + if (string.IsNullOrEmpty(instanceEvent.User.OrgId) && userId != null) + { + UserProfile up = await _profileClient.GetUserProfile((int)userId); + instanceEvent.User.NationalIdentityNumber = up.Party.SSN; } - /// - /// Process Start Current task. The main goal is to trigger the Task related business logic seperate from start process - /// - public async Task StartTask(ProcessChangeContext processChange) + return instanceEvent; + } + + private async Task HandleMoveToNext(Instance instance, ClaimsPrincipal user, string? action) + { + var processStateChange = await ProcessNext(instance, user, action); + if (processStateChange != null) { - return await _processChangeHandler.HandleStartTask(processChange); + instance = await _processEventDispatcher.UpdateProcessAndDispatchEvents(instance, new Dictionary(), processStateChange.Events); + + await _processEventDispatcher.RegisterEventWithEventsComponent(instance); } + + return processStateChange; } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngineMetricsDecorator.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngineMetricsDecorator.cs new file mode 100644 index 000000000..74a5000bd --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngineMetricsDecorator.cs @@ -0,0 +1,56 @@ +using Altinn.App.Core.Models.Process; +using Altinn.Platform.Storage.Interface.Models; +using Prometheus; + +namespace Altinn.App.Core.Internal.Process; + +/// +/// Decorator for the process engine that adds metrics for the number of processes started, ended and moved to next. +/// +public class ProcessEngineMetricsDecorator : IProcessEngine +{ + private readonly IProcessEngine _processEngine; + private static readonly Counter ProcessTaskStartCounter = Metrics.CreateCounter("altinn_app_process_start_count", "Number of tasks started", labelNames: "result" ); + private static readonly Counter ProcessTaskNextCounter = Metrics.CreateCounter("altinn_app_process_task_next_count", "Number of tasks moved to next", "result", "action", "task"); + private static readonly Counter ProcessTaskEndCounter = Metrics.CreateCounter("altinn_app_process_end_count", "Number of tasks ended", labelNames: "result"); + private static readonly Counter ProcessTimeCounter = Metrics.CreateCounter("altinn_app_process_end_time_total", "Number of seconds used to complete instances", labelNames: "result"); + + /// + /// Create a new instance of the class. + /// + /// The process engine to decorate. + public ProcessEngineMetricsDecorator(IProcessEngine processEngine) + { + _processEngine = processEngine; + } + + /// + public async Task StartProcess(ProcessStartRequest processStartRequest) + { + var result = await _processEngine.StartProcess(processStartRequest); + ProcessTaskStartCounter.WithLabels(result.Success ? "success" : "failure").Inc(); + return result; + } + + /// + public async Task Next(ProcessNextRequest request) + { + var result = await _processEngine.Next(request); + ProcessTaskNextCounter.WithLabels(result.Success ? "success" : "failure", request.Action?? "", request.Instance.Process?.CurrentTask?.ElementId ?? "").Inc(); + if(result.ProcessStateChange?.NewProcessState?.Ended != null) + { + ProcessTaskEndCounter.WithLabels(result.Success ? "success" : "failure").Inc(); + if (result.ProcessStateChange?.NewProcessState?.Started != null) + { + ProcessTimeCounter.WithLabels(result.Success ? "success" : "failure").Inc(result.ProcessStateChange.NewProcessState.Ended.Value.Subtract(result.ProcessStateChange.NewProcessState.Started.Value).TotalSeconds); + } + } + return result; + } + + /// + public async Task UpdateInstanceAndRerunEvents(ProcessStartRequest startRequest, List? events) + { + return await _processEngine.UpdateInstanceAndRerunEvents(startRequest, events); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs b/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs new file mode 100644 index 000000000..f87944693 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs @@ -0,0 +1,158 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Events; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.Platform.Storage.Interface.Enums; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Core.Internal.Process; + +/// +/// Default implementation of the process event dispatcher +/// +class ProcessEventDispatcher : IProcessEventDispatcher +{ + private readonly IInstanceClient _instanceClient; + private readonly IInstanceEventClient _instanceEventClient; + private readonly ITaskEvents _taskEvents; + private readonly IAppEvents _appEvents; + private readonly IEventsClient _eventsClient; + private readonly bool _registerWithEventSystem; + private readonly ILogger _logger; + + public ProcessEventDispatcher( + IInstanceClient instanceClient, + IInstanceEventClient instanceEventClient, + ITaskEvents taskEvents, + IAppEvents appEvents, + IEventsClient eventsClient, + IOptions appSettings, + ILogger logger) + { + _instanceClient = instanceClient; + _instanceEventClient = instanceEventClient; + _taskEvents = taskEvents; + _appEvents = appEvents; + _eventsClient = eventsClient; + _registerWithEventSystem = appSettings.Value.RegisterEventsWithEventsComponent; + _logger = logger; + } + + /// + public async Task UpdateProcessAndDispatchEvents(Instance instance, Dictionary? prefill, List? events) + { + await HandleProcessChanges(instance, events, prefill); + + // need to update the instance process and then the instance in case appbase has changed it, e.g. endEvent sets status.archived + Instance updatedInstance = await _instanceClient.UpdateProcess(instance); + await DispatchProcessEventsToStorage(updatedInstance, events); + + // remember to get the instance anew since AppBase can have updated a data element or stored something in the database. + updatedInstance = await _instanceClient.GetInstance(updatedInstance); + + return updatedInstance; + } + + /// + public async Task RegisterEventWithEventsComponent(Instance instance) + { + if (_registerWithEventSystem) + { + try + { + if (!string.IsNullOrWhiteSpace(instance.Process.CurrentTask?.ElementId)) + { + await _eventsClient.AddEvent($"app.instance.process.movedTo.{instance.Process.CurrentTask.ElementId}", instance); + } + else if (instance.Process.EndEvent != null) + { + await _eventsClient.AddEvent("app.instance.process.completed", instance); + } + } + catch (Exception exception) + { + _logger.LogWarning(exception, "Exception when sending event with the Events component"); + } + } + } + + + private async Task DispatchProcessEventsToStorage(Instance instance, List? events) + { + string org = instance.Org; + string app = instance.AppId.Split("/")[1]; + + if (events != null) + { + foreach (InstanceEvent instanceEvent in events) + { + instanceEvent.InstanceId = instance.Id; + await _instanceEventClient.SaveInstanceEvent(instanceEvent, org, app); + } + } + } + + /// + /// Will for each process change trigger relevant Process Elements to perform the relevant change actions. + /// + /// Each implementation + /// + private async Task HandleProcessChanges(Instance instance, List? events, Dictionary? prefill) + { + if (events != null) + { + foreach (InstanceEvent instanceEvent in events) + { + if (Enum.TryParse(instanceEvent.EventType, true, out InstanceEventType eventType)) + { + string? elementId = instanceEvent.ProcessInfo?.CurrentTask?.ElementId; + ITask task = GetProcessTask(instanceEvent.ProcessInfo?.CurrentTask?.AltinnTaskType); + switch (eventType) + { + case InstanceEventType.process_StartEvent: + break; + case InstanceEventType.process_StartTask: + await task.HandleTaskStart(elementId, instance, prefill); + break; + case InstanceEventType.process_EndTask: + await task.HandleTaskComplete(elementId, instance); + break; + case InstanceEventType.process_AbandonTask: + await task.HandleTaskAbandon(elementId, instance); + break; + case InstanceEventType.process_EndEvent: + await _appEvents.OnEndAppEvent(instanceEvent.ProcessInfo?.EndEvent, instance); + break; + } + } + } + } + } + + /// + /// Identify the correct task implementation + /// + /// + private ITask GetProcessTask(string? altinnTaskType) + { + if (string.IsNullOrEmpty(altinnTaskType)) + { + return new NullTask(); + } + + ITask task = new DataTask(_taskEvents); + if (altinnTaskType.Equals("confirmation")) + { + task = new ConfirmationTask(_taskEvents); + } + else if (altinnTaskType.Equals("feedback")) + { + task = new FeedbackTask(_taskEvents); + } + + return task; + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/ProcessException.cs b/src/Altinn.App.Core/Internal/Process/ProcessException.cs index 189898063..9c90c3fd2 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessException.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessException.cs @@ -5,13 +5,6 @@ namespace Altinn.App.Core.Internal.Process /// public class ProcessException : Exception { - /// - /// Initializes a new instance of the class. - /// - public ProcessException() - { - } - /// /// Initializes a new instance of the class with a specified error message. /// @@ -19,15 +12,5 @@ public ProcessException() public ProcessException(string message) : base(message) { } - - /// - /// Initializes a new instance of the class with a specified error - /// message and a reference to the inner exception that is the cause of this exception. - /// - /// The message that describes the error. - /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified - public ProcessException(string message, Exception inner) : base(message, inner) - { - } } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs b/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs new file mode 100644 index 000000000..1bb44a063 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs @@ -0,0 +1,113 @@ +using Altinn.App.Core.Features; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.Base; +using Altinn.App.Core.Models.Process; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; + +namespace Altinn.App.Core.Internal.Process; + +/// +/// Default implementation of +/// +public class ProcessNavigator : IProcessNavigator +{ + private readonly IProcessReader _processReader; + private readonly ExclusiveGatewayFactory _gatewayFactory; + private readonly ILogger _logger; + + /// + /// Initialize a new instance of + /// + /// The process reader + /// Service to fetch wanted gateway filter implementation + /// The logger + public ProcessNavigator(IProcessReader processReader, ExclusiveGatewayFactory gatewayFactory, ILogger logger) + { + _processReader = processReader; + _gatewayFactory = gatewayFactory; + _logger = logger; + } + + + /// + public async Task GetNextTask(Instance instance, string currentElement, string? action) + { + List directFlowTargets = _processReader.GetNextElements(currentElement); + List filteredNext = await NextFollowAndFilterGateways(instance, directFlowTargets, action); + if (filteredNext.Count == 0) + { + return null; + } + + if (filteredNext.Count == 1) + { + return filteredNext[0]; + } + + throw new ProcessException($"Multiple next elements found from {currentElement}. Please supply action and filters or define a default flow."); + } + + private async Task> NextFollowAndFilterGateways(Instance instance, List originNextElements, string? action) + { + List filteredNext = new List(); + foreach (var directFlowTarget in originNextElements) + { + if (directFlowTarget == null) + { + continue; + } + + if (!IsGateway(directFlowTarget)) + { + filteredNext.Add(directFlowTarget); + continue; + } + + var gateway = (ExclusiveGateway)directFlowTarget; + List outgoingFlows = _processReader.GetOutgoingSequenceFlows(directFlowTarget); + IProcessExclusiveGateway? gatewayFilter = null; + if(outgoingFlows.Any(a => a.ConditionExpression != null)) + { + gatewayFilter = _gatewayFactory.GetProcessExclusiveGateway("AltinnExpressionsExclusiveGateway"); + } else + { + gatewayFilter = _gatewayFactory.GetProcessExclusiveGateway(directFlowTarget.Id); + } + List filteredList; + if (gatewayFilter == null) + { + filteredList = outgoingFlows; + } + else + { + ProcessGatewayInformation gatewayInformation = new() + { + Action = action, + DataTypeId = gateway.ExtensionElements?.GatewayExtension?.ConnectedDataTypeId + }; + + filteredList = await gatewayFilter.FilterAsync(outgoingFlows, instance, gatewayInformation); + } + var defaultSequenceFlow = filteredList.Find(s => s.Id == gateway.Default); + if (defaultSequenceFlow != null) + { + var defaultTarget = _processReader.GetFlowElement(defaultSequenceFlow.TargetRef); + filteredNext.AddRange(await NextFollowAndFilterGateways(instance, new List { defaultTarget }, action)); + } + else + { + var filteredTargets = filteredList.Select(e => _processReader.GetFlowElement(e.TargetRef)).ToList(); + filteredNext.AddRange(await NextFollowAndFilterGateways(instance, filteredTargets, action)); + } + } + _logger.LogDebug("Filtered next elements: {FilteredNextElements}", string.Join(", ", filteredNext.Select(e => e.Id))); + return filteredNext; + } + + + private static bool IsGateway(ProcessElement processElement) + { + return processElement is ExclusiveGateway; + } +} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs index 9314102e2..0d2b3d9fb 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs @@ -1,5 +1,4 @@ using System.Xml.Serialization; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.Base; @@ -13,14 +12,14 @@ public class ProcessReader : IProcessReader private readonly Definitions _definitions; /// - /// Create instance of ProcessReader where process stream is fetched from + /// Create instance of ProcessReader where process stream is fetched from /// - /// Implementation of IProcess used to get stream of BPMN process + /// Implementation of IProcessClient used to get stream of BPMN process /// If BPMN file could not be deserialized - public ProcessReader(IProcess processService) + public ProcessReader(IProcessClient processClient) { XmlSerializer serializer = new XmlSerializer(typeof(Definitions)); - Definitions? definitions = (Definitions?)serializer.Deserialize(processService.GetProcessDefinition()); + Definitions? definitions = (Definitions?)serializer.Deserialize(processClient.GetProcessDefinition()); _definitions = definitions ?? throw new InvalidOperationException("Failed to deserialize BPMN definitions. Definitions was null"); } @@ -103,73 +102,10 @@ public List GetSequenceFlowIds() return GetSequenceFlows().Select(s => s.Id).ToList(); } - /// - public List GetNextElements(string? currentElementId) - { - EnsureArgumentNotNull(currentElementId, nameof(currentElementId)); - List nextElements = new List(); - List allElements = GetAllFlowElements(); - if (!allElements.Exists(e => e.Id == currentElementId)) - { - throw new ProcessException($"Unable to find a element using element id {currentElementId}."); - } - - foreach (SequenceFlow sequenceFlow in GetSequenceFlows().FindAll(s => s.SourceRef == currentElementId)) - { - nextElements.AddRange(allElements.FindAll(e => sequenceFlow.TargetRef == e.Id)); - } - - return nextElements; - } - - /// - public List GetNextElementIds(string? currentElement) - { - return GetNextElements(currentElement).Select(e => e.Id).ToList(); - } - - /// - public List GetOutgoingSequenceFlows(ProcessElement? flowElement) - { - if (flowElement == null) - { - return new List(); - } - - return GetSequenceFlows().FindAll(sf => flowElement.Outgoing.Contains(sf.Id)).ToList(); - } - - /// - public List GetSequenceFlowsBetween(string? currentStepId, string? nextElementId) - { - List flowsToReachTarget = new List(); - foreach (SequenceFlow sequenceFlow in _definitions.Process.SequenceFlow.FindAll(s => s.SourceRef == currentStepId)) - { - if (sequenceFlow.TargetRef.Equals(nextElementId)) - { - flowsToReachTarget.Add(sequenceFlow); - return flowsToReachTarget; - } - - if (_definitions.Process.ExclusiveGateway != null && _definitions.Process.ExclusiveGateway.FirstOrDefault(g => g.Id == sequenceFlow.TargetRef) != null) - { - List subGatewayFlows = GetSequenceFlowsBetween(sequenceFlow.TargetRef, nextElementId); - if (subGatewayFlows.Any()) - { - flowsToReachTarget.Add(sequenceFlow); - flowsToReachTarget.AddRange(subGatewayFlows); - return flowsToReachTarget; - } - } - } - - return flowsToReachTarget; - } - /// public ProcessElement? GetFlowElement(string? elementId) { - EnsureArgumentNotNull(elementId, nameof(elementId)); + ArgumentNullException.ThrowIfNull(elementId); ProcessTask? task = _definitions.Process.Tasks.Find(t => t.Id == elementId); if (task != null) @@ -191,31 +127,39 @@ public List GetSequenceFlowsBetween(string? currentStepId, string? return _definitions.Process.ExclusiveGateway.Find(e => e.Id == elementId); } - + /// - public ElementInfo? GetElementInfo(string? elementId) + public List GetNextElements(string? currentElementId) { - var e = GetFlowElement(elementId); - if (e == null || e is ExclusiveGateway) + ArgumentNullException.ThrowIfNull(currentElementId); + List nextElements = new List(); + List allElements = GetAllFlowElements(); + if (!allElements.Exists(e => e.Id == currentElementId)) { - return null; + throw new ProcessException($"Unable to find a element using element id {currentElementId}."); } - ElementInfo elementInfo = new ElementInfo() - { - Id = e.Id, - Name = e.Name, - ElementType = e.ElementType() - }; - if (e is ProcessTask task) + foreach (SequenceFlow sequenceFlow in GetSequenceFlows().FindAll(s => s.SourceRef == currentElementId)) { - elementInfo.AltinnTaskType = task.TaskType; + nextElements.AddRange(allElements.FindAll(e => sequenceFlow.TargetRef == e.Id)); } - return elementInfo; + return nextElements; } - private List GetAllFlowElements() + /// + public List GetOutgoingSequenceFlows(ProcessElement? flowElement) + { + if (flowElement == null) + { + return new List(); + } + + return GetSequenceFlows().FindAll(sf => flowElement.Outgoing.Contains(sf.Id)).ToList(); + } + + /// + public List GetAllFlowElements() { List flowElements = new List(); flowElements.AddRange(GetStartEvents()); @@ -224,10 +168,4 @@ private List GetAllFlowElements() flowElements.AddRange(GetEndEvents()); return flowElements; } - - private static void EnsureArgumentNotNull(object? argument, string paramName) - { - if (argument == null) - throw new ArgumentNullException(paramName); - } } diff --git a/src/Altinn.App.Core/Internal/Profile/IProfileClient.cs b/src/Altinn.App.Core/Internal/Profile/IProfileClient.cs new file mode 100644 index 000000000..17206dd2a --- /dev/null +++ b/src/Altinn.App.Core/Internal/Profile/IProfileClient.cs @@ -0,0 +1,17 @@ +using Altinn.Platform.Profile.Models; + +namespace Altinn.App.Core.Internal.Profile +{ + /// + /// Interface for profile functionality + /// + public interface IProfileClient + { + /// + /// Method for getting the userprofile from a given user id + /// + /// the user id + /// The userprofile for the given user id + Task GetUserProfile(int userId); + } +} diff --git a/src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs b/src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs new file mode 100644 index 000000000..b6c38ecdb --- /dev/null +++ b/src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs @@ -0,0 +1,24 @@ +using Altinn.Platform.Register.Models; + +namespace Altinn.App.Core.Internal.Registers +{ + /// + /// Interface for register functionality + /// + public interface IAltinnPartyClient + { + /// + /// Returns party information + /// + /// The partyId + /// The party for the given partyId + Task GetParty(int partyId); + + /// + /// Looks up a party by person or organisation number. + /// + /// A populated lookup object with information about what to look for. + /// The party lookup containing either SSN or organisation number. + Task LookupParty(PartyLookup partyLookup); + } +} diff --git a/src/Altinn.App.Core/Internal/Registers/IOrganizationClient.cs b/src/Altinn.App.Core/Internal/Registers/IOrganizationClient.cs new file mode 100644 index 000000000..5b18de260 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Registers/IOrganizationClient.cs @@ -0,0 +1,17 @@ +using Altinn.Platform.Register.Models; + +namespace Altinn.App.Core.Internal.Registers +{ + /// + /// Interface for the entity registry (ER: Enhetsregisteret) + /// + public interface IOrganizationClient + { + /// + /// Method for getting an organization based on a organization nr + /// + /// the organization number + /// The organization for the given organization number + Task GetOrganization(string OrgNr); + } +} diff --git a/src/Altinn.App.Core/Internal/Registers/IPersonClient.cs b/src/Altinn.App.Core/Internal/Registers/IPersonClient.cs new file mode 100644 index 000000000..726f88e0c --- /dev/null +++ b/src/Altinn.App.Core/Internal/Registers/IPersonClient.cs @@ -0,0 +1,25 @@ +#nullable enable + +using Altinn.Platform.Register.Models; + +namespace Altinn.App.Core.Internal.Registers +{ + /// + /// Describes the required methods for an implementation of a person repository client. + /// + public interface IPersonClient + { + /// + /// Get the object for the person identified with the parameters. + /// + /// + /// The method requires both the national identity number and the last name of the person. This is used to + /// verify that entered information is correct and to prevent testing of random identity numbers. + /// + /// The national identity number of the person. + /// The last name of the person. + /// The cancellation token to cancel operation. + /// The identified person if found. + Task GetPerson(string nationalIdentityNumber, string lastName, CancellationToken ct); + } +} diff --git a/src/Altinn.App.Core/Internal/Secrets/ISecretsClient.cs b/src/Altinn.App.Core/Internal/Secrets/ISecretsClient.cs new file mode 100644 index 000000000..0fcd06e1a --- /dev/null +++ b/src/Altinn.App.Core/Internal/Secrets/ISecretsClient.cs @@ -0,0 +1,38 @@ +using Microsoft.Azure.KeyVault; +using Microsoft.Azure.KeyVault.WebKey; + +namespace Altinn.App.Core.Internal.Secrets +{ + /// + /// Interface for secrets service + /// + public interface ISecretsClient + { + /// + /// Gets the latest version of a key from key vault. + /// + /// The name of the key. + /// The key as a JSON web key. + Task GetKeyAsync(string keyName); + + /// + /// Gets the latest version of a secret from key vault. + /// + /// The name of the secret. + /// The secret value. + Task GetSecretAsync(string secretName); + + /// + /// Gets the latest version of a certificate from key vault. + /// + /// The name of certificate. + /// The certificate as a byte array. + Task GetCertificateAsync(string certificateName); + + /// + /// Gets the key vault client. + /// + /// The key vault client. + KeyVaultClient GetKeyVaultClient(); + } +} diff --git a/src/Altinn.App.Core/Internal/Sign/ISignClient.cs b/src/Altinn.App.Core/Internal/Sign/ISignClient.cs new file mode 100644 index 000000000..c7185cc6b --- /dev/null +++ b/src/Altinn.App.Core/Internal/Sign/ISignClient.cs @@ -0,0 +1,16 @@ +using Altinn.App.Core.Models; + +namespace Altinn.App.Core.Internal.Sign; + +/// +/// Interface for httpClient to send sign requests to platform +/// +public interface ISignClient +{ + /// + /// Generate a signature for a list of DataElements for a user + /// + /// The context for the signature + /// + public Task SignDataElements(SignatureContext signatureContext); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs b/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs new file mode 100644 index 000000000..d4635c138 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs @@ -0,0 +1,117 @@ +using Altinn.App.Core.Models; + +namespace Altinn.App.Core.Internal.Sign; + +/// +/// Context for a signature of DataElements +/// +public class SignatureContext +{ + /// + /// Create a new signing context for one data element + /// + /// Identifier for the instance containing the data elements to sign + /// The id of the DataType where the signature should be stored + /// The signee + /// The data element to sign + public SignatureContext(InstanceIdentifier instanceIdentifier, string signatureDataTypeId, Signee signee, params DataElementSignature[] dataElementSignature) + { + InstanceIdentifier = instanceIdentifier; + SignatureDataTypeId = signatureDataTypeId; + DataElementSignatures.AddRange(dataElementSignature); + Signee = signee; + } + + /// + /// Create a new signing context for multiple data elements + /// + /// Identifier for the instance containing the data elements to sign + /// The id of the DataType where the signature should be stored + /// The signee + /// The data elements to sign + public SignatureContext(InstanceIdentifier instanceIdentifier, string signatureDataTypeId, Signee signee, List dataElementSignatures) + { + InstanceIdentifier = instanceIdentifier; + SignatureDataTypeId = signatureDataTypeId; + DataElementSignatures = dataElementSignatures; + Signee = signee; + } + + /// + /// The id of the DataType where the signature should be stored + /// + public string SignatureDataTypeId { get; } + + /// + /// Identifier for the instance containing the data elements to sign + /// + public InstanceIdentifier InstanceIdentifier { get; } + + /// + /// List of DataElements and whether they are signed or not + /// + public List DataElementSignatures { get; } = new (); + + /// + /// The user performing the signing + /// + public Signee Signee { get; } +} + +/// +/// Object representing the user performing the signing +/// +public class Signee +{ + /// + /// User id of the user performing the signing + /// + public string UserId { get; set; } + + /// + /// The SSN of the user performing the signing, set if the signer is a person + /// + public string? PersonNumber { get; set; } + + /// + /// The organisation number of the user performing the signing, set if the signer is an organisation + /// + public string? OrganisationNumber { get; set; } +} + +/// +/// Object representing a data element and whether it is signed or not +/// +public class DataElementSignature +{ + /// + /// Create a new data element where the signed status is set to true + /// + /// ID of the DataElement that should be included in the signature + public DataElementSignature(string dataElementId) + { + DataElementId = dataElementId; + Signed = true; + } + + /// + /// Create a new data element where the signed status is set to the value of the signed parameter + /// + /// ID of the DataElement that should be included in the signature + /// Whether the DataElement is signed or not + public DataElementSignature(string dataElementId, bool signed) + { + DataElementId = dataElementId; + Signed = signed; + } + + /// + /// ID of the DataElement that should be included in the signature + /// + public string DataElementId { get; } + + /// + /// Whether the DataElement is signed or not + /// + public bool Signed { get; } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Texts/IText.cs b/src/Altinn.App.Core/Internal/Texts/IText.cs index 67c592564..b4851b8e0 100644 --- a/src/Altinn.App.Core/Internal/Texts/IText.cs +++ b/src/Altinn.App.Core/Internal/Texts/IText.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Internal.Texts /// /// Describes the public methods of a text resources service /// + [Obsolete("Use IAppResources.GetTexts() instead")] public interface IText { /// @@ -14,6 +15,6 @@ public interface IText /// Application identifier which is unique within an organisation. /// Language for the text resource /// The text resource - Task GetText(string org, string app, string language); + Task GetText(string org, string app, string language); } } diff --git a/src/Altinn.App.Core/Models/ApplicationMetadata.cs b/src/Altinn.App.Core/Models/ApplicationMetadata.cs index edb20e29c..db28abbf0 100644 --- a/src/Altinn.App.Core/Models/ApplicationMetadata.cs +++ b/src/Altinn.App.Core/Models/ApplicationMetadata.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Reflection; using Altinn.Platform.Storage.Interface.Models; using Newtonsoft.Json; @@ -58,5 +58,17 @@ public ApplicationMetadata(string id) /// [JsonProperty(PropertyName = "logo")] public Logo? Logo { get; set; } + + /// + /// Frontend sometimes need to have knowledge of the nuget package version for backwards compatibility + /// + [JsonProperty(PropertyName = "altinnNugetVersion")] + public string AltinnNugetVersion { get; set; } = typeof(ApplicationMetadata).Assembly!.GetName().Version!.ToString(); + + /// + /// Holds properties that are not mapped to other properties + /// + [System.Text.Json.Serialization.JsonExtensionData] + public Dictionary? UnmappedProperties { get; set; } } } diff --git a/src/Altinn.App.Core/Models/CalculationResult.cs b/src/Altinn.App.Core/Models/CalculationResult.cs index 66eda5053..e3e644980 100644 --- a/src/Altinn.App.Core/Models/CalculationResult.cs +++ b/src/Altinn.App.Core/Models/CalculationResult.cs @@ -1,3 +1,5 @@ +#nullable enable + using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Models @@ -28,7 +30,7 @@ public CalculationResult(DataElement dataElement) /// /// The DataElement base object /// The changed fields - public CalculationResult(DataElement dataElement, Dictionary changedFields) + public CalculationResult(DataElement dataElement, Dictionary changedFields) { MapDataElementToCalculationResult(dataElement); ChangedFields = changedFields; @@ -37,7 +39,7 @@ public CalculationResult(DataElement dataElement, Dictionary cha /// /// The key-value pair of fields changed by a calculation /// - public Dictionary ChangedFields { get; set; } + public Dictionary? ChangedFields { get; set; } private void MapDataElementToCalculationResult(DataElement dataElement) { diff --git a/src/Altinn.App.Core/Models/Expressions/ExpressionFunctionEnum.cs b/src/Altinn.App.Core/Models/Expressions/ExpressionFunctionEnum.cs index b5be86c35..a3097aebd 100644 --- a/src/Altinn.App.Core/Models/Expressions/ExpressionFunctionEnum.cs +++ b/src/Altinn.App.Core/Models/Expressions/ExpressionFunctionEnum.cs @@ -105,4 +105,12 @@ public enum ExpressionFunction /// Return true if the single argument evaluate to false, otherwise return false /// not, -} \ No newline at end of file + /// + /// Returns a positional argument + /// + argv, + /// + /// Get the action performed in task prior to bpmn gateway + /// + gatewayAction, +} diff --git a/src/Altinn.App.Core/Models/Layout/Components/OptionsComponent.cs b/src/Altinn.App.Core/Models/Layout/Components/OptionsComponent.cs index 26e91baf7..b77c574eb 100644 --- a/src/Altinn.App.Core/Models/Layout/Components/OptionsComponent.cs +++ b/src/Altinn.App.Core/Models/Layout/Components/OptionsComponent.cs @@ -52,10 +52,9 @@ public class OptionsSource /// /// Constructor for /// - public OptionsSource(string group, string label, string value) + public OptionsSource(string group, string value) { Group = group; - Label = label; Value = value; } /// @@ -63,36 +62,6 @@ public OptionsSource(string group, string label, string value) /// public string Group { get; } /// - /// a reference to a text id to be used as the label for each iteration of the group - /// - /// - /// As for the label property, we have to define a text resource that can be used as a label for each repetition of the group. - /// This follows similar syntax as the value, and will also be familiar if you have used variables in text. - /// - /// - /// The referenced text resource must use variables to read text from individual fields - /// { - /// "language": "nb", - /// "resources": [ - /// { - /// "id": "dropdown.label", - /// "value": "Person: {0}, Age: {1}", - /// "variables": [ - /// { - /// "key": "some.group[{0}].name", - /// "dataSource": "dataModel.default" - /// }, - /// { - /// "key": "some.group[{0}].age", - /// "dataSource": "dataModel.default" - /// } - /// ] - /// } - /// ] - /// } - /// - public string Label { get; } - /// /// a reference to a field in the group that should be used as the option value. Notice that we set up this [{0}] syntax. Here the {0} will be replaced by each index of the group. /// /// diff --git a/src/Altinn.App.Core/Models/Layout/Components/SummaryComponent.cs b/src/Altinn.App.Core/Models/Layout/Components/SummaryComponent.cs index da2b072b4..b42e08eb8 100644 --- a/src/Altinn.App.Core/Models/Layout/Components/SummaryComponent.cs +++ b/src/Altinn.App.Core/Models/Layout/Components/SummaryComponent.cs @@ -16,19 +16,13 @@ public class SummaryComponent : BaseComponent /// public string ComponentRef { get; set; } - /// - /// Name of the page this summary component references - /// - public string PageRef { get; set; } - /// /// Constructor /// - public SummaryComponent(string id, string type, Expression? hidden, string componentRef, string pageRef, IReadOnlyDictionary? additionalProperties) : + public SummaryComponent(string id, string type, Expression? hidden, string componentRef, IReadOnlyDictionary? additionalProperties) : base(id, type, null, hidden, null, null, additionalProperties) { ComponentRef = componentRef; - PageRef = pageRef; } } diff --git a/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs b/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs index 607bdc391..59f23c33d 100644 --- a/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs +++ b/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs @@ -234,7 +234,6 @@ private BaseComponent ReadComponent(ref Utf8JsonReader reader, JsonSerializerOpt int maxCount = 1; // > 1 is repeating, but might not be specified for non-repeating groups // Custom properties for Summary string? componentRef = null; - string? pageRef = null; // Custom properties for components with optionId or literal options string? optionId = null; List? literalOptions = null; @@ -294,9 +293,6 @@ private BaseComponent ReadComponent(ref Utf8JsonReader reader, JsonSerializerOpt case "componentref": componentRef = reader.GetString(); break; - case "pageref": - pageRef = reader.GetString(); - break; // option case "optionsid": optionId = reader.GetString(); @@ -321,6 +317,17 @@ private BaseComponent ReadComponent(ref Utf8JsonReader reader, JsonSerializerOpt switch (type.ToLowerInvariant()) { + case "repeatinggroup": + ThrowJsonExceptionIfNull(children, "Component with \"type\": \"Group\" requires a \"children\" property"); + if (!(dataModelBindings?.ContainsKey("group") ?? false)) + { + throw new JsonException($"A repeating group id:\"{id}\" does not have a \"group\" dataModelBinding"); + } + + var directRepComponent = new RepeatingGroupComponent(id, type, dataModelBindings, new List(), children, maxCount, hidden, hiddenRow, required, readOnly, additionalProperties); + return directRepComponent; + + case "group": ThrowJsonExceptionIfNull(children, "Component with \"type\": \"Group\" requires a \"children\" property"); @@ -343,8 +350,8 @@ private BaseComponent ReadComponent(ref Utf8JsonReader reader, JsonSerializerOpt var gridComponent = new GridComponent(id, type, dataModelBindings, new List(), children, hidden, required, readOnly, additionalProperties); return gridComponent; case "summary": - ValidateSummary(componentRef, pageRef); - return new SummaryComponent(id, type, hidden, componentRef, pageRef, additionalProperties); + ValidateSummary(componentRef); + return new SummaryComponent(id, type, hidden, componentRef, additionalProperties); case "checkboxes": case "radiobuttons": case "dropdown": @@ -384,11 +391,11 @@ private static void ValidateOptions(string? optionId, List? literalOp } } - private static void ValidateSummary([NotNull] string? componentRef, [NotNull] string? pageRef) + private static void ValidateSummary([NotNull] string? componentRef) { - if (componentRef is null || pageRef is null) + if (componentRef is null) { - throw new JsonException("Component with \"type\": \"Summary\" requires \"componentRef\" and \"pageRef\" properties"); + throw new JsonException("Component with \"type\": \"Summary\" requires the \"componentRef\" property"); } } diff --git a/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs b/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs new file mode 100644 index 000000000..4ae14a0a6 --- /dev/null +++ b/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs @@ -0,0 +1,47 @@ +namespace Altinn.App.Core.Models.Process +{ + /// + /// Class representing the result of a process change + /// + public class ProcessChangeResult + { + /// + /// Gets or sets a value indicating whether the process change was successful + /// + public bool Success { get; set; } + /// + /// Gets or sets the error message if the process change was not successful + /// + public string? ErrorMessage { get; set; } + /// + /// Gets or sets the error type if the process change was not successful + /// + public ProcessErrorType? ErrorType { get; set; } + + /// + /// Gets or sets the process state change if the process change was successful + /// + public ProcessStateChange? ProcessStateChange { get; set; } + } + + /// + /// Types of errors that can occur during a process change + /// + public enum ProcessErrorType + { + /// + /// The process change was not allowed due to the current state of the process + /// + Conflict, + + /// + /// The process change lead to an internal error + /// + Internal, + + /// + /// The user is not authorized to perform the process change + /// + Unauthorized + } +} diff --git a/src/Altinn.App.Core/Models/Process/ProcessGatewayInformation.cs b/src/Altinn.App.Core/Models/Process/ProcessGatewayInformation.cs new file mode 100644 index 000000000..ca6f13d03 --- /dev/null +++ b/src/Altinn.App.Core/Models/Process/ProcessGatewayInformation.cs @@ -0,0 +1,17 @@ +namespace Altinn.App.Core.Models.Process; + +/// +/// Additional information about the gateway in the context of a running process +/// +public class ProcessGatewayInformation +{ + /// + /// The action performed to reach the gateway + /// + public string? Action { get; set; } + + /// + /// The datatype associated with the gateway + /// + public string? DataTypeId { get; set; } +} diff --git a/src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs b/src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs new file mode 100644 index 000000000..5f02ca231 --- /dev/null +++ b/src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs @@ -0,0 +1,23 @@ +using System.Security.Claims; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Models.Process; + +/// +/// Class that defines the request for moving the process to the next task +/// +public class ProcessNextRequest +{ + /// + /// The instance to be moved to the next task + /// + public Instance Instance { get; set; } + /// + /// The user that is performing the action + /// + public ClaimsPrincipal User { get; set; } + /// + /// The action that is performed + /// + public string? Action { get; set; } +} diff --git a/src/Altinn.App.Core/Models/Process/ProcessStartRequest.cs b/src/Altinn.App.Core/Models/Process/ProcessStartRequest.cs new file mode 100644 index 000000000..bfa6c1642 --- /dev/null +++ b/src/Altinn.App.Core/Models/Process/ProcessStartRequest.cs @@ -0,0 +1,31 @@ +using System.Security.Claims; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Models.Process; + +/// +/// Class that defines the request for starting a new process +/// +public class ProcessStartRequest +{ + /// + /// The instance to be started + /// + public Instance Instance { get; set; } + /// + /// The user that is starting the process + /// + public ClaimsPrincipal User { get; set; } + /// + /// The prefill data supplied when starting the process + /// + public Dictionary? Prefill { get; set; } + /// + /// The start event id, only needed if multiple start events in process + /// + public string? StartEventId { get; set; } + /// + /// If set to true the instance is not updated and the events are not dispatched + /// + public bool Dryrun { get; set; } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Models/ProcessStateChange.cs b/src/Altinn.App.Core/Models/Process/ProcessStateChange.cs similarity index 67% rename from src/Altinn.App.Core/Models/ProcessStateChange.cs rename to src/Altinn.App.Core/Models/Process/ProcessStateChange.cs index f5f59e64f..fcca5ac3c 100644 --- a/src/Altinn.App.Core/Models/ProcessStateChange.cs +++ b/src/Altinn.App.Core/Models/Process/ProcessStateChange.cs @@ -1,8 +1,6 @@ -using System.Collections.Generic; - using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models.Process { /// /// Represents a change in process state for an instance. @@ -12,16 +10,16 @@ public class ProcessStateChange /// /// Gets or sets the old process state /// - public ProcessState OldProcessState { get; set; } + public ProcessState? OldProcessState { get; set; } /// /// Gets or sets the new process state /// - public ProcessState NewProcessState { get; set; } + public ProcessState? NewProcessState { get; set; } /// /// Gets or sets a list of events to be registered. /// - public List Events { get; set; } + public List? Events { get; set; } } } diff --git a/src/Altinn.App.Core/Models/ProcessChangeContext.cs b/src/Altinn.App.Core/Models/ProcessChangeContext.cs deleted file mode 100644 index faa693e0d..000000000 --- a/src/Altinn.App.Core/Models/ProcessChangeContext.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Security.Claims; -using Altinn.App.Core.Internal.Process; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Models -{ - /// - /// Data entity that will floow between Process Api, Process Engine, Process Handlers and the TaskImpl/Gateway implt - /// - public class ProcessChangeContext - { - /// - /// Initializes a new instance of the class. - /// - public ProcessChangeContext(Instance instance, ClaimsPrincipal user) - { - Instance = instance; - User = user; - } - - /// - /// The current instance - /// - public Instance Instance { get; set; } - - /// - /// The request process element Id - /// - public string? RequestedProcessElementId { get; set; } - - /// - /// The process flow - /// - public List ProcessFlowElements { get; set; } = new List(); - - /// - /// Information messages - /// - public List ProcessMessages { get; set; } - - /// - /// Did process change fail? - /// - public bool FailedProcessChange { get; set; } - - /// - /// The identity performing the process change - /// - public ClaimsPrincipal User { get; set; } - - /// - /// ProcessStateChange - /// - public ProcessStateChange ProcessStateChange { get; set; } - - /// - /// The current process element to be processed - /// - public string ElementToBeProcessed { get; set; } - - /// - /// Process prefill - /// - public Dictionary Prefill { get; set; } - - /// - /// The ProcessSequenceFlowType - /// - public ProcessSequenceFlowType ProcessSequenceFlowType { get; set; } - - /// - /// Defines if the process handler should not handle events - /// - public bool DontUpdateProcessAndDispatchEvents { get; set; } - } -} diff --git a/src/Altinn.App.Core/Models/ProcessChangeInfo.cs b/src/Altinn.App.Core/Models/ProcessChangeInfo.cs deleted file mode 100644 index 5a2e3b9e1..000000000 --- a/src/Altinn.App.Core/Models/ProcessChangeInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Altinn.App.Core.Models -{ - /// - /// Process change info containing information passed around between process engine componentes - /// - public class ProcessChangeInfo - { - /// - /// Type message - /// - public string Type { get; set; } - - /// - /// The message itself - /// - public string Message { get; set; } - } -} diff --git a/src/Altinn.App.Core/Models/UserAction/ActionError.cs b/src/Altinn.App.Core/Models/UserAction/ActionError.cs new file mode 100644 index 000000000..1b249832e --- /dev/null +++ b/src/Altinn.App.Core/Models/UserAction/ActionError.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace Altinn.App.Core.Models.UserAction; + +/// +/// Defines an error object that should be returned if the action fails +/// +public class ActionError +{ + /// + /// Machine readable error code + /// + [JsonPropertyName("code")] + public string? Code { get; set; } + + /// + /// Human readable error message or text key + /// + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + /// + /// Error metadata + /// + [JsonPropertyName("metadata")] + public Dictionary? Metadata { get; set; } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Models/UserAction/ClientAction.cs b/src/Altinn.App.Core/Models/UserAction/ClientAction.cs new file mode 100644 index 000000000..5b1aeeaa5 --- /dev/null +++ b/src/Altinn.App.Core/Models/UserAction/ClientAction.cs @@ -0,0 +1,62 @@ +using System.Text.Json.Serialization; + +namespace Altinn.App.Core.Models.UserAction; + +/// +/// Defines an action that should be performed by the client +/// +public class ClientAction +{ + /// + /// Name of the action + /// + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// + /// Metadata for the action + /// + [JsonPropertyName("metadata")] + public Dictionary? Metadata { get; set; } + + /// + /// Creates a nextPage client action + /// + /// + public static ClientAction NextPage() + { + var frontendAction = new ClientAction() + { + Name = "nextPage" + }; + return frontendAction; + } + + /// + /// Creates a previousPage client action + /// + /// + public static ClientAction PreviousPage() + { + var frontendAction = new ClientAction() + { + Name = "previousPage" + }; + return frontendAction; + } + + /// + /// Creates a navigateToPage client action + /// + /// The page that should be navigated to + /// + public static ClientAction NavigateToPage(string page) + { + var frontendAction = new ClientAction() + { + Name = "navigateToPage", + Metadata = new Dictionary { { "page", page } } + }; + return frontendAction; + } +} diff --git a/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs b/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs new file mode 100644 index 000000000..a76293229 --- /dev/null +++ b/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs @@ -0,0 +1,44 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Models.UserAction; + +/// +/// Context for user actions +/// +public class UserActionContext +{ + /// + /// Creates a new instance of the class + /// + /// The instance the action is performed on + /// The user performing the action + /// The id of the button that triggered the action (optional) + /// + public UserActionContext(Instance instance, int userId, string? buttonId = null, Dictionary? actionMetadata = null) + { + Instance = instance; + UserId = userId; + ButtonId = buttonId; + ActionMetadata = actionMetadata ?? new Dictionary(); + } + + /// + /// The instance the action is performed on + /// + public Instance Instance { get; } + + /// + /// The user performing the action + /// + public int UserId { get; } + + /// + /// The id of the button that triggered the action (optional) + /// + public string? ButtonId { get; } + + /// + /// Additional metadata for the action + /// + public Dictionary ActionMetadata { get; } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs b/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs new file mode 100644 index 000000000..1ffeba500 --- /dev/null +++ b/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs @@ -0,0 +1,76 @@ +using System.Net; +using System.Runtime.Serialization; +using Altinn.App.Core.Models.Validation; + +namespace Altinn.App.Core.Models.UserAction; + +/// +/// Represents the result of a user action +/// +public class UserActionResult +{ + /// + /// Gets or sets a value indicating whether the user action was a success + /// + public bool Success { get; set; } + + /// + /// Gets or sets a dictionary of updated data models. Key should be dataTypeId + /// + public Dictionary? UpdatedDataModels { get; set; } + + /// + /// Actions for the client to perform after the user action has been handled + /// + public List? ClientActions { get; set; } + + /// + /// Validation issues that should be displayed to the user + /// + public ActionError? Error { get; set; } + + /// + /// Creates a success result + /// + /// + /// + public static UserActionResult SuccessResult(List? clientActions = null) + { + var userActionResult = new UserActionResult + { + Success = true, + ClientActions = clientActions + }; + return userActionResult; + } + + /// + /// Creates a failure result + /// + /// + /// + /// + public static UserActionResult FailureResult(ActionError error, List? clientActions = null) + { + return new UserActionResult + { + Success = false, + ClientActions = clientActions, + Error = error + }; + } + + /// + /// Adds an updated data model to the result + /// + /// + /// + public void AddUpdatedDataModel(string dataModelId, object? dataModel) + { + if (UpdatedDataModels == null) + { + UpdatedDataModels = new Dictionary(); + } + UpdatedDataModels.Add(dataModelId, dataModel); + } +} diff --git a/src/Altinn.App.Core/Models/Validation/ExpressionValidation.cs b/src/Altinn.App.Core/Models/Validation/ExpressionValidation.cs new file mode 100644 index 000000000..b88b2af6d --- /dev/null +++ b/src/Altinn.App.Core/Models/Validation/ExpressionValidation.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Serialization; +using Altinn.App.Core.Models.Expressions; + + +namespace Altinn.App.Core.Models.Validation +{ + + /// + /// Resolved expression validation + /// + public class ExpressionValidation + { + /// + public string? Message { get; set; } + + /// + public Expression? Condition { get; set; } + + /// + public ValidationIssueSeverity? Severity { get; set; } + } + + /// + /// Raw expression validation or definition from the validation configuration file + /// + public class RawExpressionValidation + { + /// + public string? Message { get; set; } + + /// + public Expression? Condition { get; set; } + + /// + [JsonConverter(typeof(FrontendSeverityConverter))] + public ValidationIssueSeverity? Severity { get; set; } + + /// + public string? Ref { get; set; } + } +} diff --git a/src/Altinn.App.Core/Models/Validation/FrontendSeverityConverter.cs b/src/Altinn.App.Core/Models/Validation/FrontendSeverityConverter.cs new file mode 100644 index 000000000..2db22f938 --- /dev/null +++ b/src/Altinn.App.Core/Models/Validation/FrontendSeverityConverter.cs @@ -0,0 +1,42 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Altinn.App.Core.Models.Validation +{ + /// + public class FrontendSeverityConverter : JsonConverter + { + /// + public override ValidationIssueSeverity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException(); + } + + return reader.GetString() switch + { + "errors" => ValidationIssueSeverity.Error, + "warnings" => ValidationIssueSeverity.Warning, + "info" => ValidationIssueSeverity.Informational, + "success" => ValidationIssueSeverity.Success, + _ => throw new JsonException(), + }; + } + + /// + public override void Write(Utf8JsonWriter writer, ValidationIssueSeverity value, JsonSerializerOptions options) + { + string output = value switch + { + ValidationIssueSeverity.Error => "errors", + ValidationIssueSeverity.Warning => "warnings", + ValidationIssueSeverity.Informational => "info", + ValidationIssueSeverity.Success => "success", + _ => throw new JsonException(), + }; + + JsonSerializer.Serialize(writer, output, options); + } + } +} diff --git a/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs b/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs index c10b85366..07865ce12 100644 --- a/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs +++ b/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs @@ -1,5 +1,7 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.App.Core.Features.Validation; using Newtonsoft.Json; -using Newtonsoft.Json.Converters; namespace Altinn.App.Core.Models.Validation { @@ -11,50 +13,83 @@ public class ValidationIssue /// /// The seriousness of the identified issue. /// + /// + /// This property is serialized in json as a number + /// 1: Error (something needs to be fixed) + /// 2: Warning (does not prevent submission) + /// 3: Information (hint shown to the user) + /// 4: Fixed (obsolete, only used for v3 of frontend) + /// 5: Success (Inform the user that something was completed with success) + /// [JsonProperty(PropertyName = "severity")] - [JsonConverter(typeof(StringEnumConverter))] - public ValidationIssueSeverity Severity { get; set; } + [JsonPropertyName("severity")] + [System.Text.Json.Serialization.JsonConverter(typeof(JsonNumberEnumConverter))] + public required ValidationIssueSeverity Severity { get; set; } /// /// The unique id of the specific element with the identified issue. /// - [JsonProperty(PropertyName = "instanceId")] + [System.Text.Json.Serialization.JsonIgnore] + [Newtonsoft.Json.JsonIgnore] + [Obsolete("Not in use", error: true)] public string? InstanceId { get; set; } /// - /// The uniqe id of the data element of a given instance with the identified issue. + /// The unique id of the data element of a given instance with the identified issue. /// [JsonProperty(PropertyName = "dataElementId")] + [JsonPropertyName("dataElementId")] public string? DataElementId { get; set; } /// - /// A reference to a property the issue is a bout. + /// A reference to a property the issue is about. /// [JsonProperty(PropertyName = "field")] + [JsonPropertyName("field")] public string? Field { get; set; } /// /// A system readable identification of the type of issue. + /// Eg: /// [JsonProperty(PropertyName = "code")] + [JsonPropertyName("code")] public string? Code { get; set; } /// /// A human readable description of the issue. /// [JsonProperty(PropertyName = "description")] + [JsonPropertyName("description")] public string? Description { get; set; } /// - /// The validation source of the issue eg. File, Schema, Component + /// The short name of the class that crated the message (set automatically after return of list) /// + /// + /// Intentionally not marked as "required", because it is set in + /// [JsonProperty(PropertyName = "source")] - public string? Source { get; set; } + [JsonPropertyName("source")] + public string Source { get; set; } = default!; /// /// The custom text key to use for the localized text in the frontend. /// [JsonProperty(PropertyName = "customTextKey")] + [JsonPropertyName("customTextKey")] public string? CustomTextKey { get; set; } + + /// + /// might include some parameters (typically the field value, or some derived value) + /// that should be included in error message. + /// + /// + /// The localized text for the key might be "Date must be between {0} and {1}" + /// and the param will provide the dynamical range of allowable dates (eg teh reporting period) + /// + [JsonProperty(PropertyName = "customTextParams")] + [JsonPropertyName("customTextParams")] + public List? CustomTextParams { get; set; } } } diff --git a/src/Altinn.App.Core/Models/Validation/ValidationIssueSeverity.cs b/src/Altinn.App.Core/Models/Validation/ValidationIssueSeverity.cs index 5661998d7..2b2264492 100644 --- a/src/Altinn.App.Core/Models/Validation/ValidationIssueSeverity.cs +++ b/src/Altinn.App.Core/Models/Validation/ValidationIssueSeverity.cs @@ -28,6 +28,7 @@ public enum ValidationIssueSeverity /// /// The issue has been corrected. /// + [Obsolete("We run all validations from frontend version 4, so we don't need info about fixed issues")] Fixed = 4, /// diff --git a/src/Altinn.App.Core/Models/Validation/ValidationIssueSource.cs b/src/Altinn.App.Core/Models/Validation/ValidationIssueSource.cs index 532119033..45054aa97 100644 --- a/src/Altinn.App.Core/Models/Validation/ValidationIssueSource.cs +++ b/src/Altinn.App.Core/Models/Validation/ValidationIssueSource.cs @@ -25,5 +25,10 @@ public static class ValidationIssueSources /// Required field validation /// public static readonly string Custom = nameof(Custom); + + /// + /// Expression validation + /// + public static readonly string Expression = nameof(Expression); } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 8bf4bff1d..b1f3f0a3c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -7,10 +7,10 @@ $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) - preview + preview.0 v true - 10.0 + 12 diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index dda4dc735..be3c00ec5 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -1,11 +1,4 @@ - - - all - runtime; build; native; contentfiles; analyzers - - - $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) diff --git a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj index 48be4bbb9..151e0a19b 100644 --- a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj +++ b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj @@ -1,16 +1,21 @@ - net6.0 + net8.0 enable enable false + $(NoWarn);CS1591;CS0618 + - - + + diff --git a/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs new file mode 100644 index 000000000..e51df365b --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs @@ -0,0 +1,233 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using Altinn.App.Api.Models; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Api.Tests.Utils; +using Altinn.App.Core.Features; +using Altinn.App.Core.Models.UserAction; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Altinn.App.Api.Tests.Controllers; + +public class ActionsControllerTests : ApiTestBase, IClassFixture> +{ + public ActionsControllerTests(WebApplicationFactory factory) : base(factory) + { + } + + [Fact] + public async Task Perform_returns_403_if_user_not_authorized() + { + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(1000, null, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"lookup_unauthorized\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Perform_returns_401_if_user_not_authenticated() + { + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"lookup_unauthorized\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Perform_returns_401_if_userId_is_null() + { + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(null, null, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"lookup_unauthorized\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Perform_returns_400_if_action_is_null() + { + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(1000, null, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":null}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Fact] + public async Task Perform_returns_409_if_process_not_started() + { + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef43"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(1000, null, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.Conflict); + } + + [Fact] + public async Task Perform_returns_409_if_process_ended() + { + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef42"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(1000, null, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.Conflict); + } + + [Fact] + public async Task Perform_returns_200_if_action_succeeded() + { + OverrideServicesForThisTest = (services) => { services.AddTransient(); }; + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(1000, null, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var content = await response.Content.ReadAsStringAsync(); + var expectedString = """ + { + "updatedDataModels": null, + "clientActions": [ + { + "name": "nextPage", + "metadata": null + } + ], + "error": null + } + """; + CompareResult(expectedString, content); + } + + [Fact] + public async Task Perform_returns_400_if_action_failed() + { + OverrideServicesForThisTest = (services) => { services.AddTransient(); }; + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(1001, null, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Fact] + public async Task Perform_returns_404_if_action_implementation_not_found() + { + OverrideServicesForThisTest = (services) => { services.AddTransient(); }; + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(1001, null, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"notfound\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + + //TODO: replace this assertion with a proper one once fluentassertions has a json compare feature scheduled for v7 https://github.com/fluentassertions/fluentassertions/issues/2205 + private static void CompareResult(string expectedString, string actualString) + { + T? expected = JsonSerializer.Deserialize(expectedString); + T? actual = JsonSerializer.Deserialize(actualString); + actual.Should().BeEquivalentTo(expected); + } +} + +public class LookupAction : IUserAction +{ + public string Id => "lookup"; + + public async Task HandleAction(UserActionContext context) + { + await Task.CompletedTask; + if (context.UserId == 1000) + { + return UserActionResult.SuccessResult(new List() { ClientAction.NextPage() }); + } + + return UserActionResult.FailureResult(new ActionError()); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/ApplicationMetadataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ApplicationMetadataControllerTests.cs new file mode 100644 index 000000000..ac03d6b89 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/ApplicationMetadataControllerTests.cs @@ -0,0 +1,39 @@ +using System.Text.Json; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Models; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Altinn.App.Api.Tests.Constants; + +public class ApplicationMetadataControllerTests +{ + private readonly WebApplicationFactory _factory = new(); + private readonly Mock _appMetadataMock = new(); + + [Fact] + public async Task VeryfyExtraFieldsInApplicationMetadataIsPreserved() + { + var org = "tdd"; + var appId = "test-app"; + var appMetadataSample = $"{{\"id\":\"{org}/{appId}\",\"org\":\"{org}\",\"title\":{{\"nb\":\"Bestillingseksempelapp\"}},\"dataTypes\":[],\"partyTypesAllowed\":{{}},\"extra_Unknown_list\":[3,\"tre\",{{\"verdi\":3}}]}}"; + var application = JsonSerializer.Deserialize(appMetadataSample, new JsonSerializerOptions(JsonSerializerDefaults.Web))!; + _appMetadataMock.Setup(m => m.GetApplicationMetadata()).ReturnsAsync(application); + using var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => + { + services.AddTransient(sp => _appMetadataMock.Object); + }); + }).CreateClient(); + + var response = await client.GetStringAsync($"/{org}/{appId}/api/v1/applicationmetadata"); + + // Assert that unknonwn parts of json is preserved + response.Should().ContainAll("extra_Unknown_list", "verdi\":3"); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs index cb73343c7..c888ab9f8 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs @@ -9,6 +9,7 @@ using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; namespace Altinn.App.Api.Tests.Controllers { @@ -18,6 +19,29 @@ public DataControllerTests(WebApplicationFactory factory) : base(factor { } + [Fact] + public async Task PutDataElement_MissingDataType_ReturnsBadRequest() + { + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 1337; + Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + TestData.DeleteInstance(org, app, instanceOwnerPartyId, guid); + TestData.PrepareInstance(org, app, instanceOwnerPartyId, guid); + + + using var content = new StringContent("{}", System.Text.Encoding.UTF8, "application/json"); // empty valid json + var response = await client.PostAsync($"/{org}/{app}/instances/{instanceOwnerPartyId}/{guid}/data", content); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + var responseContent = await response.Content.ReadAsStringAsync(); + responseContent.Should().Contain("dataType"); + } + [Fact] public async Task CreateDataElement_BinaryPdf_AnalyserShouldRunOk() { diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_PatchFormDataImplementation.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchFormDataImplementation.cs new file mode 100644 index 000000000..bbf2a97a5 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchFormDataImplementation.cs @@ -0,0 +1,156 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using Altinn.App.Api.Controllers; +using Altinn.App.Api.Models; +using Altinn.App.Core.Features; +using Altinn.App.Core.Features.FileAnalyzis; +using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Prefill; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.FeatureManagement; +using Moq; +using Xunit; +using DataType = Altinn.Platform.Storage.Interface.Models.DataType; + +namespace Altinn.App.Api.Tests.Controllers; + +public class DataController_PatchFormDataImplementation : IAsyncDisposable +{ + // Test data + static readonly Guid DataGuid = new("12345678-1234-1234-1234-123456789123"); + private readonly Instance _instance = new(); + + // Service mocks + private readonly Mock> _dLoggerMock = new(MockBehavior.Loose); + private readonly Mock> _vLoggerMock = new(MockBehavior.Loose); + private readonly Mock _instanceClientMock = new(MockBehavior.Strict); + private readonly Mock _instantiationProcessorMock = new(MockBehavior.Strict); + private readonly Mock _dataClientMock = new (MockBehavior.Strict); + private readonly Mock _dataProcessorMock = new(MockBehavior.Strict); + private readonly Mock _appModelMock = new (MockBehavior.Strict); + private readonly Mock _appResourcesServiceMock = new (MockBehavior.Strict); + private readonly Mock _prefillServiceMock = new (MockBehavior.Strict); + private readonly Mock _fileAnalyserServiceMock = new (MockBehavior.Strict); + private readonly Mock _fileValidationServiceMock = new (MockBehavior.Strict); + private readonly Mock _appMetadataMock = new (MockBehavior.Strict); + private readonly Mock _featureManageMock = new (MockBehavior.Strict); + + // ValidatorMocks + private readonly Mock _formDataValidator = new(MockBehavior.Strict); + private readonly Mock _dataElementValidator = new(MockBehavior.Strict); + + // System under test + private readonly ServiceCollection _serviceCollection = new(); + private readonly DataController _dataController; + private readonly ServiceProvider _serviceProvider; + + public DataController_PatchFormDataImplementation() + { + _formDataValidator.Setup(fdv => fdv.DataType).Returns(_dataType.Id); + _formDataValidator.Setup(fdv => fdv.ValidationSource).Returns("formDataValidator"); + _formDataValidator.Setup(fdv => fdv.HasRelevantChanges(It.IsAny(), It.IsAny())).Returns(true); + // _dataElementValidator.Setup(ev => ev.DataType).Returns(_dataType.Id); + _serviceCollection.AddSingleton(_formDataValidator.Object); + _serviceCollection.AddSingleton(_dataElementValidator); + _serviceProvider = _serviceCollection.BuildServiceProvider(); + var validationService = new ValidationService( + _serviceProvider, + _dataClientMock.Object, + _appModelMock.Object, + _appMetadataMock.Object, + _vLoggerMock.Object + ); + _dataController = new DataController( + _dLoggerMock.Object, + _instanceClientMock.Object, + _instantiationProcessorMock.Object, + _dataClientMock.Object, + new List (){_dataProcessorMock.Object}, + _appModelMock.Object, + _appResourcesServiceMock.Object, + _prefillServiceMock.Object, + validationService, + _fileAnalyserServiceMock.Object, + _fileValidationServiceMock.Object, + _appMetadataMock.Object, + _featureManageMock.Object + ); + } + + private readonly DataType _dataType = new() + { + Id = "dataTypeId", + }; + + private readonly DataElement _dataElement = new() + { + Id = DataGuid.ToString(), + DataType = "dataTypeId" + }; + + private class MyModel + { + [MinLength(20)] + public string? Name { get; set; } + } + + [Fact] + public async Task Test() + { + var request = JsonSerializer.Deserialize(""" + { + "patch": [ + { + "op": "replace", + "path": "/Name", + "value": "Test Testesen" + } + ], + "ignoredValidators": [ + "required" + ] + } + """)!; + var oldModel = new MyModel { Name = "OrginaltNavn" }; + var validationIssues = new List() + { + new () + { + Severity = ValidationIssueSeverity.Error, + Description = "First error", + } + }; + + _dataProcessorMock.Setup(d => d.ProcessDataWrite(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns((Instance i, Guid j, MyModel data, MyModel? oldData) => Task.CompletedTask); + _formDataValidator.Setup(fdv => fdv.ValidateFormData( + It.Is(i => i == _instance), + It.Is(de=>de == _dataElement), + It.IsAny())) + .ReturnsAsync(validationIssues); + + // Act + var (response, _) = await _dataController.PatchFormDataImplementation(_dataType, _dataElement, request, oldModel, _instance); + + // Assert + response.Should().NotBeNull(); + response.NewDataModel.Should().BeOfType().Subject.Name.Should().Be("Test Testesen"); + var validator = response.ValidationIssues.Should().ContainSingle().Which; + validator.Key.Should().Be("formDataValidator"); + var issue = validator.Value.Should().ContainSingle().Which; + issue.Description.Should().Be("First error"); + _dataProcessorMock.Verify(d => d.ProcessDataWrite(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + } + + public async ValueTask DisposeAsync() + { + await _serviceProvider.DisposeAsync(); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs new file mode 100644 index 000000000..cefc9e274 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs @@ -0,0 +1,343 @@ +using Altinn.App.Api.Tests.Utils; +using Microsoft.AspNetCore.Mvc.Testing; +using System.Net.Http.Headers; +using System.Net; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Altinn.App.Api.Models; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models; +using Altinn.App.Core.Features; +using Xunit; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Json.More; +using Json.Patch; +using Json.Pointer; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit.Abstractions; + +namespace Altinn.App.Api.Tests.Controllers; + +public class DataControllerPatchTests : ApiTestBase, IClassFixture> +{ + // Define constants + private const string Org = "tdd"; + private const string App = "contributer-restriction"; + private const int InstanceOwnerPartyId = 500600; + private static readonly Guid InstanceGuid = new("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); + private static readonly string InstanceId = $"{InstanceOwnerPartyId}/{InstanceGuid}"; + private static readonly Guid DataGuid = new("fc121812-0336-45fb-a75c-490df3ad5109"); + + // Define mocks + private readonly Mock _dataProcessorMock = new(); + private readonly Mock _formDataValidatorMock = new(); + + private static readonly JsonSerializerOptions JsonSerializerOptions = new () + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + UnknownTypeHandling = JsonUnknownTypeHandling.JsonElement, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; + + private readonly ITestOutputHelper _outputHelper; + + // Constructor with common setup + public DataControllerPatchTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) : base(factory) + { + _formDataValidatorMock.Setup(v => v.DataType).Returns("Not a valid data type"); + _outputHelper = outputHelper; + OverrideServicesForAllTests = (services) => + { + services.AddSingleton(_dataProcessorMock.Object); + services.AddSingleton(_formDataValidatorMock.Object); + }; + TestData.DeleteInstanceAndData(Org, App, InstanceOwnerPartyId, InstanceGuid); + TestData.PrepareInstance(Org, App, InstanceOwnerPartyId, InstanceGuid); + } + + // Helper method to call the API + private async Task<(HttpResponseMessage response, string responseString, TResponse parsedResponse)> CallPatchApi(JsonPatch patch, List? ignoredValidators, HttpStatusCode expectedStatus) + { + _outputHelper.WriteLine($"Calling PATCH /{Org}/{App}/instances/{InstanceId}/data/{DataGuid}"); + using var httpClient = GetRootedClient(Org, App); + string token = PrincipalUtil.GetToken(1337, null); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + var serializedPatch = JsonSerializer.Serialize(new DataPatchRequest() + { + Patch = patch, + IgnoredValidators = ignoredValidators, + }, JsonSerializerOptions); + _outputHelper.WriteLine(serializedPatch); + using var updateDataElementContent = + new StringContent(serializedPatch, System.Text.Encoding.UTF8, "application/json"); + var response = await httpClient.PatchAsync($"/{Org}/{App}/instances/{InstanceId}/data/{DataGuid}", updateDataElementContent); + var responseString = await response.Content.ReadAsStringAsync(); + using var responseParsedRaw = JsonDocument.Parse(responseString); + _outputHelper.WriteLine("\nResponse:"); + _outputHelper.WriteLine(JsonSerializer.Serialize(responseParsedRaw, JsonSerializerOptions)); + response.Should().HaveStatusCode(expectedStatus); + var responseObject = JsonSerializer.Deserialize(responseString, JsonSerializerOptions)!; + return (response, responseString, responseObject); + } + + + [Fact] + public async Task ValidName_ReturnsOk() + { + // Update data element + var patch = new JsonPatch( + PatchOperation.Replace(JsonPointer.Create("melding", "name"), JsonNode.Parse("\"Ola Olsen\""))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.OK); + + parsedResponse.ValidationIssues.Should().ContainKey("Required").WhoseValue.Should().BeEmpty(); + + var newModelElement = parsedResponse.NewDataModel.Should().BeOfType().Which; + var newModel = newModelElement.Deserialize()!; + newModel.Melding.Name.Should().Be("Ola Olsen"); + + _dataProcessorMock.Verify(p => p.ProcessDataWrite(It.IsAny(), It.Is(dataId => dataId == DataGuid), It.IsAny(), It.IsAny()), Times.Exactly(1)); + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task NullName_ReturnsOkAndValidationError() + { + // Update data element + var patch = new JsonPatch( + PatchOperation.Test(JsonPointer.Create("melding", "name"), JsonNode.Parse("null")), + PatchOperation.Replace(JsonPointer.Create("melding", "name"), JsonNode.Parse("null"))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.OK); + + var requiredList = parsedResponse.ValidationIssues.Should().ContainKey("Required").WhoseValue; + var requiredName = requiredList.Should().ContainSingle().Which; + requiredName.Field.Should().Be("melding.name"); + requiredName.Description.Should().Be("melding.name is required in component with id name"); + + var newModelElement = parsedResponse.NewDataModel.Should().BeOfType().Which; + var newModel = newModelElement.Deserialize()!; + newModel.Melding.Name.Should().BeNull(); + + _dataProcessorMock.Verify(p => p.ProcessDataWrite(It.IsAny(), It.Is(dataId => dataId == DataGuid), It.IsAny(), It.IsAny()), Times.Exactly(1)); + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task InvalidTestValue_ReturnsPreconditionFailed() + { + // Update data element + var patch = new JsonPatch( + PatchOperation.Test(JsonPointer.Create("melding", "name"), JsonNode.Parse("\"Not correct previous value\"")), + PatchOperation.Replace(JsonPointer.Create("melding", "name"), JsonNode.Parse("null"))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.PreconditionFailed); + + parsedResponse.Detail.Should().Be("Path `/melding/name` is not equal to the indicated value."); + + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task InvalidTestPath_ReturnsPreconditionFailed() + { + // Update data element + var patch = new JsonPatch( + PatchOperation.Test(JsonPointer.Create("melding", "name-error"), JsonNode.Parse("null")), + PatchOperation.Replace(JsonPointer.Create("melding", "name"), JsonNode.Parse("null"))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.UnprocessableContent); + + parsedResponse.Detail.Should().Be("Path `/melding/name-error` could not be reached."); + + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task InvalidJsonPointer_ReturnsUnprocessableContent() + { + // Update data element + var pointer = JsonPointer.Create("not", "a pointer"); + var patch = new JsonPatch( + PatchOperation.Test(pointer, JsonNode.Parse("null")), + PatchOperation.Replace(pointer, JsonNode.Parse("\"Ivar\""))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.UnprocessableContent); + + parsedResponse.Detail.Should().Be("Path `/not/a pointer` could not be reached."); + + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task TestEmptyListAndInsertElement_ReturnsNewModel() + { + // Update data element + var pointer = JsonPointer.Create("melding", "nested_list"); + var patch = new JsonPatch( + PatchOperation.Test(pointer, JsonNode.Parse("""[]""")), + PatchOperation.Add(pointer, JsonNode.Parse("""[{"key": "newKey"}]"""))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.OK); + + var newModel = parsedResponse.NewDataModel.Should().BeOfType().Which.Deserialize()!; + var listItem = newModel.Melding.NestedList.Should().ContainSingle().Which; + listItem.Key.Should().Be("newKey"); + + parsedResponse.ValidationIssues + .Should().ContainKey("Required").WhoseValue + .Should().Contain(i => i.Field == "melding.name"); + + _dataProcessorMock.Verify( + p => p.ProcessDataWrite( + It.IsAny(), + It.Is(dataId => dataId == DataGuid), + It.Is(s=>s.Melding.NestedList.Count == 1), + It.Is(s=> s!.Melding.NestedList.Count == 0) + ), Times.Exactly(1)); + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task AddItemToNonInitializedList_ReturnsUnprocessableEntity() + { + // This test fails to initialize the list, thus creating an error + // Added this test to ensure that a change in behaviour (when changing json patch library) + // is detected + var pointer = JsonPointer.Create("melding", "nested_list", 0, "newKey"); + var patch = new JsonPatch( + PatchOperation.Add(pointer, JsonNode.Parse("\"newValue\""))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.UnprocessableContent); + + parsedResponse.Detail.Should().Contain("/melding/nested_list/0/newKey"); + + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task InsertNonExistingFieldWithoutTest_ReturnsUnprocessableContent() + { + // Update data element + var pointer = JsonPointer.Create("melding", "non_existing_field"); + var patch = new JsonPatch( + PatchOperation.Add(pointer, JsonNode.Parse("""[{"key": "newKey"}]"""))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.UnprocessableContent); + + parsedResponse.Detail.Should().Contain("The JSON property 'non_existing_field' could not be mapped to any .NET member contained in type"); + + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateContainerWithListProperty_ReturnsCorrectDataModel() + { + var pointer = JsonPointer.Create("melding", "nested_list"); + var createFirstElementPatch = new JsonPatch( + PatchOperation.Test(pointer, JsonNode.Parse("[]")), + PatchOperation.Add(pointer.Combine("-"), JsonNode.Parse("""{"key": "myKey" }""")) + ); + + var (_, _, firstResponse) = await CallPatchApi(createFirstElementPatch, null, HttpStatusCode.OK); + + var firstData = firstResponse.NewDataModel.Should().BeOfType().Which; + var firstListItem = firstData.GetProperty("melding").GetProperty("nested_list").EnumerateArray().First(); + firstListItem.GetProperty("values").GetArrayLength().Should().Be(0); + + var addValuePatch = new JsonPatch( + PatchOperation.Test(pointer.Combine("0"), firstListItem.AsNode()), + PatchOperation.Remove(pointer.Combine("0"))); + var (_, _, secondResponse) = await CallPatchApi(addValuePatch, null, HttpStatusCode.OK); + var secondData = secondResponse.NewDataModel.Should().BeOfType().Which; + secondData.GetProperty("melding").GetProperty("nested_list").GetArrayLength().Should().Be(0); + } + + [Fact] + public async Task RemoveStringProperty_ReturnsCorrectDataModel() + { + var pointer = JsonPointer.Create("melding", "name"); + var createFirstElementPatch = new JsonPatch( + PatchOperation.Test(pointer, JsonNode.Parse("null")), + PatchOperation.Add(pointer, JsonNode.Parse("\"myValue\"")), + PatchOperation.Remove(pointer) + ); + + var (_, _, firstResponse) = await CallPatchApi(createFirstElementPatch, null, HttpStatusCode.OK); + + var firstData = firstResponse.NewDataModel.Should().BeOfType().Which; + var firstListItem = firstData.GetProperty("melding").GetProperty("name"); + firstListItem.ValueKind.Should().Be(JsonValueKind.Null); + + var addValuePatch = new JsonPatch( + PatchOperation.Test(pointer, firstListItem.AsNode()), + PatchOperation.Replace(pointer, JsonNode.Parse("\"mySecondValue\""))); + var (_, _, secondResponse) = await CallPatchApi(addValuePatch, null, HttpStatusCode.OK); + var secondData = secondResponse.NewDataModel.Should().BeOfType().Which; + var secondValue = secondData.GetProperty("melding").GetProperty("name"); + secondValue.GetString().Should().Be("mySecondValue"); + } + + [Fact] + public async Task SetStringPropertyToEmtpy_ReturnsCorrectDataModel() + { + var pointer = JsonPointer.Create("melding", "name"); + var createFirstElementPatch = new JsonPatch( + PatchOperation.Test(pointer, JsonNode.Parse("null")), + PatchOperation.Add(pointer, JsonNode.Parse("\"\"")) + ); + + var (_, _, firstResponse) = await CallPatchApi(createFirstElementPatch, null, HttpStatusCode.OK); + + var firstData = firstResponse.NewDataModel.Should().BeOfType().Which; + var firstListItem = firstData.GetProperty("melding").GetProperty("name"); + firstListItem.ValueKind.Should().Be(JsonValueKind.Null);; + + var addValuePatch = new JsonPatch( + PatchOperation.Test(pointer, firstListItem.AsNode()), + PatchOperation.Replace(pointer, JsonNode.Parse("\"mySecondValue\""))); + var (_, _, secondResponse) = await CallPatchApi(addValuePatch, null, HttpStatusCode.OK); + var secondData = secondResponse.NewDataModel.Should().BeOfType().Which; + var secondValue = secondData.GetProperty("melding").GetProperty("name"); + secondValue.GetString().Should().Be("mySecondValue"); + } + + [Fact] + public async Task SetAttributeTagPropertyToEmtpy_ReturnsCorrectDataModel() + { + var pointer = JsonPointer.Create("melding", "tag-with-attribute"); + var createFirstElementPatch = new JsonPatch( + PatchOperation.Test(pointer, JsonNode.Parse("null")), + PatchOperation.Add(pointer, JsonNode.Parse("""{"value": "" }""")) + ); + + var (_, _, firstResponse) = await CallPatchApi(createFirstElementPatch, null, HttpStatusCode.OK); + + var firstData = firstResponse.NewDataModel.Should().BeOfType().Which; + var firstListItem = firstData.GetProperty("melding").GetProperty("tag-with-attribute"); + firstListItem.GetProperty("value").ValueKind.Should().Be(JsonValueKind.Null); + + var addValuePatch = new JsonPatch( + PatchOperation.Test(pointer, firstListItem.AsNode()), + PatchOperation.Replace(pointer.Combine("value"), JsonNode.Parse("null"))); + var (_, _, secondResponse) = await CallPatchApi(addValuePatch, null, HttpStatusCode.OK); + var secondData = secondResponse.NewDataModel.Should().BeOfType().Which; + var secondValue = secondData.GetProperty("melding").GetProperty("name"); + secondValue.ValueKind.Should().Be(JsonValueKind.Null); + } + + [Fact] + public async Task ValidationIssueSeverity_IsSerializedNumeric() + { + var patch = new JsonPatch(); + var (_, responseString, _) = await CallPatchApi(patch, null, HttpStatusCode.OK); + + responseString.Should().Contain("\"severity\":1"); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs new file mode 100644 index 000000000..f04fea560 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs @@ -0,0 +1,156 @@ +using Altinn.App.Api.Tests.Utils; +using Microsoft.AspNetCore.Mvc.Testing; +using System.Net.Http.Headers; +using System.Net; +using System.Text.Json; +using Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models; +using Altinn.App.Core.Features; +using Xunit; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Moq; + +namespace Altinn.App.Api.Tests.Controllers; + +public class DataController_PutTests : ApiTestBase, IClassFixture> +{ + private readonly Mock _dataProcessor = new(); + + private static readonly JsonSerializerOptions JsonSerializerOptions = new () + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + public DataController_PutTests(WebApplicationFactory factory) : base(factory) + { + OverrideServicesForAllTests = (services) => + { + services.AddSingleton(_dataProcessor.Object); + }; + } + + [Fact] + public async Task PutDataElement_TestSinglePartUpdate_ReturnsOk() + { + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 501337; + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetToken(1337, null); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Create instance + var createResponse = + await client.PostAsync($"{org}/{app}/instances/?instanceOwnerPartyId={instanceOwnerPartyId}", null); + var createResponseContent = await createResponse.Content.ReadAsStringAsync(); + createResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var createResponseParsed = JsonSerializer.Deserialize(createResponseContent, JsonSerializerOptions)!; + var instanceId = createResponseParsed.Id; + + // Create data element (not sure why it isn't created when the instance is created, autoCreate is true) + using var createDataElementContent = + new StringContent("""{"melding":{"name": "Ivar"}}""", System.Text.Encoding.UTF8, "application/json"); + var createDataElementResponse = + await client.PostAsync($"/{org}/{app}/instances/{instanceId}/data?dataType=default", createDataElementContent); + var createDataElementResponseContent = await createDataElementResponse.Content.ReadAsStringAsync(); + createDataElementResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var createDataElementResponseParsed = + JsonSerializer.Deserialize(createDataElementResponseContent, JsonSerializerOptions)!; + var dataGuid = createDataElementResponseParsed.Id; + + // Update data element + using var updateDataElementContent = + new StringContent("""{"melding":{"name": "Ola Olsen"}}""", System.Text.Encoding.UTF8, "application/json"); + var response = await client.PutAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}", updateDataElementContent); + response.StatusCode.Should().Be(HttpStatusCode.Created); + + // Verify stored data + var readDataElementResponse = await client.GetAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}"); + readDataElementResponse.StatusCode.Should().Be(HttpStatusCode.OK); + var readDataElementResponseContent = await readDataElementResponse.Content.ReadAsStringAsync(); + var readDataElementResponseParsed = + JsonSerializer.Deserialize(readDataElementResponseContent)!; + readDataElementResponseParsed.Melding.Name.Should().Be("Ola Olsen"); + + _dataProcessor.Verify(p => p.ProcessDataRead(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny()), Times.Exactly(1)); + _dataProcessor.Verify(p => p.ProcessDataWrite(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny(), It.IsAny()), Times.Exactly(1)); // TODO: Shouldn't this be 2 because of the first write? + _dataProcessor.VerifyNoOtherCalls(); + } + + [Fact] + public async Task PutDataElement_TestMultiPartUpdateWithCustomDataProcessor_ReturnsOk() + { + // Run the previous test with a custom data processor + _dataProcessor.Setup(d => d.ProcessDataWrite(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((Instance instance, Guid dataGuid, object data, object previousData) => + { + if (data is Skjema skjema) + { + skjema.Melding.Toggle = true; + } + + return Task.CompletedTask; + }); + + // Run previous test with different setup + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 501337; + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetToken(1337, null); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Create instance + var createResponse = + await client.PostAsync($"{org}/{app}/instances/?instanceOwnerPartyId={instanceOwnerPartyId}", null); + var createResponseContent = await createResponse.Content.ReadAsStringAsync(); + createResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var createResponseParsed = JsonSerializer.Deserialize(createResponseContent, JsonSerializerOptions)!; + var instanceId = createResponseParsed.Id; + + // Create data element (not sure why it isn't created when the instance is created, autoCreate is true) + using var createDataElementContent = + new StringContent("""{"melding":{"name": "Ivar"}}""", System.Text.Encoding.UTF8, "application/json"); + var createDataElementResponse = + await client.PostAsync($"/{org}/{app}/instances/{instanceId}/data?dataType=default", + createDataElementContent); + var createDataElementResponseContent = await createDataElementResponse.Content.ReadAsStringAsync(); + createDataElementResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var createDataElementResponseParsed = + JsonSerializer.Deserialize(createDataElementResponseContent, JsonSerializerOptions)!; + var dataGuid = createDataElementResponseParsed.Id; + + // Verify stored data + var firstReadDataElementResponse = await client.GetAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}"); + var firstReadDataElementResponseContent = await firstReadDataElementResponse.Content.ReadAsStringAsync(); + var firstReadDataElementResponseParsed = + JsonSerializer.Deserialize(firstReadDataElementResponseContent)!; + firstReadDataElementResponseParsed.Melding.Name.Should().Be("Ivar"); + firstReadDataElementResponseParsed.Melding.Toggle.Should().BeFalse(); + + // Update data element + using var updateDataElementContent = + new StringContent("""{"melding":{"name": "Ola Olsen"}}""", System.Text.Encoding.UTF8, "application/json"); + var response = await client.PutAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}", updateDataElementContent); + response.StatusCode.Should().Be(HttpStatusCode.SeeOther); + + + // Verify stored data + var readDataElementResponse = await client.GetAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}"); + var readDataElementResponseContent = await readDataElementResponse.Content.ReadAsStringAsync(); + var readDataElementResponseParsed = + JsonSerializer.Deserialize(readDataElementResponseContent)!; + readDataElementResponseParsed.Melding.Name.Should().Be("Ola Olsen"); + readDataElementResponseParsed.Melding.Toggle.Should().BeTrue(); + + _dataProcessor.Verify(p=>p.ProcessDataRead(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny()), Times.Exactly(2)); + _dataProcessor.Verify(p => p.ProcessDataWrite(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny(), It.IsAny()), Times.Exactly(1)); // TODO: Shouldn't this be 2 because of the first write? + _dataProcessor.VerifyNoOtherCalls(); + + } + + +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs index 904a0c05f..73614b011 100644 --- a/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs @@ -28,7 +28,7 @@ public EventsReceiverControllerTests(WebApplicationFactory factory) public async Task Post_ValidEventType_ShouldReturnOk() { var client = _factory.CreateClient(); - string token = PrincipalUtil.GetToken(1337); + string token = PrincipalUtil.GetToken(1337, null); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); CloudEvent cloudEvent = new() { @@ -58,7 +58,7 @@ public async Task Post_ValidEventType_ShouldReturnOk() public async Task Post_NonValidEventType_ShouldReturnBadRequest() { var client = _factory.CreateClient(); - string token = PrincipalUtil.GetToken(1337); + string token = PrincipalUtil.GetToken(1337, null); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); CloudEvent cloudEvent = new() { diff --git a/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs index cf6ce4412..51e522b0a 100644 --- a/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs @@ -1,63 +1,64 @@ using Altinn.App.Api.Controllers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Instances; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Microsoft.AspNetCore.Mvc; using Moq; using Xunit; -namespace Altinn.App.Api.Tests.Controllers; - -public class FileScanControllerTests +namespace Altinn.App.Api.Tests.Controllers { - [Fact] - public async Task InstanceAndDataExists_ShouldReturn200Ok() + public class FileScanControllerTests { - const string org = "org"; - const string app = "app"; - const int instanceOwnerPartyId = 12345; - Guid instanceId = Guid.NewGuid(); - Mock instanceClientMock = CreateInstanceClientMock(org, app, instanceOwnerPartyId, instanceId); + [Fact] + public async Task InstanceAndDataExists_ShouldReturn200Ok() + { + const string org = "org"; + const string app = "app"; + const int instanceOwnerPartyId = 12345; + Guid instanceId = Guid.NewGuid(); + Mock instanceClientMock = CreateInstanceClientMock(org, app, instanceOwnerPartyId, instanceId); - var fileScanController = new FileScanController(instanceClientMock.Object); - var fileScanResults = await fileScanController.GetFileScanResults(org, app, instanceOwnerPartyId, instanceId); + var fileScanController = new FileScanController(instanceClientMock.Object); + var fileScanResults = await fileScanController.GetFileScanResults(org, app, instanceOwnerPartyId, instanceId); - fileScanResults.Result.Should().BeOfType(); - fileScanResults.Value?.FileScanResult.Should().Be(Platform.Storage.Interface.Enums.FileScanResult.Infected); - } + fileScanResults.Result.Should().BeOfType(); + fileScanResults.Value?.FileScanResult.Should().Be(Platform.Storage.Interface.Enums.FileScanResult.Infected); + } - [Fact] - public async Task InstanceDoesNotExists_ShouldReturnNotFound() - { - const string org = "org"; - const string app = "app"; - const int instanceOwnerPartyId = 12345; - Guid instanceId = Guid.NewGuid(); - Mock instanceClientMock = CreateInstanceClientMock(org, app, instanceOwnerPartyId, instanceId); + [Fact] + public async Task InstanceDoesNotExists_ShouldReturnNotFound() + { + const string org = "org"; + const string app = "app"; + const int instanceOwnerPartyId = 12345; + Guid instanceId = Guid.NewGuid(); + Mock instanceClientMock = CreateInstanceClientMock(org, app, instanceOwnerPartyId, instanceId); - var fileScanController = new FileScanController(instanceClientMock.Object); - var fileScanResults = await fileScanController.GetFileScanResults(org, app, instanceOwnerPartyId, Guid.NewGuid()); + var fileScanController = new FileScanController(instanceClientMock.Object); + var fileScanResults = await fileScanController.GetFileScanResults(org, app, instanceOwnerPartyId, Guid.NewGuid()); - fileScanResults.Result.Should().BeOfType(); - } + fileScanResults.Result.Should().BeOfType(); + } - private static Mock CreateInstanceClientMock(string org, string app, int instanceOwnerPartyId, Guid instanceId) - { - var instance = new Instance + private static Mock CreateInstanceClientMock(string org, string app, int instanceOwnerPartyId, Guid instanceId) { - Id = $"{instanceOwnerPartyId}/{instanceId}", - Process = null, - Data = new List() + var instance = new Instance { - new() { Id = Guid.NewGuid().ToString(), FileScanResult = Platform.Storage.Interface.Enums.FileScanResult.Infected } - } - }; + Id = $"{instanceOwnerPartyId}/{instanceId}", + Process = null, + Data = new List() + { + new() { Id = Guid.NewGuid().ToString(), FileScanResult = Platform.Storage.Interface.Enums.FileScanResult.Infected } + } + }; - var instanceClientMock = new Mock(); - instanceClientMock - .Setup(e => e.GetInstance(app, org, instanceOwnerPartyId, instanceId)) - .Returns(Task.FromResult(instance)); + var instanceClientMock = new Mock(); + instanceClientMock + .Setup(e => e.GetInstance(app, org, instanceOwnerPartyId, instanceId)) + .Returns(Task.FromResult(instance)); - return instanceClientMock; + return instanceClientMock; + } } } diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs index 62e43ecbf..22407bb7e 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs @@ -2,7 +2,6 @@ using Altinn.App.Api.Controllers; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.AppModel; using Altinn.Common.PEP.Interfaces; using Altinn.Platform.Storage.Interface.Models; @@ -14,27 +13,35 @@ using Xunit; using Altinn.App.Api.Models; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Events; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Prefill; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; +using IProcessEngine = Altinn.App.Core.Internal.Process.IProcessEngine; namespace Altinn.App.Api.Tests.Controllers; public class InstancesController_ActiveInstancesTest { private readonly Mock> _logger = new(); - private readonly Mock _registrer = new(); - private readonly Mock _instanceClient = new(); - private readonly Mock _data = new(); + private readonly Mock _registrer = new(); + private readonly Mock _instanceClient = new(); + private readonly Mock _data = new(); private readonly Mock _appMetadata = new(); private readonly Mock _appModel = new(); private readonly Mock _instantiationProcessor = new(); private readonly Mock _instantiationValidator = new(); private readonly Mock _pdp = new(); - private readonly Mock _eventsService = new(); + private readonly Mock _eventsService = new(); private readonly IOptions _appSettings = Options.Create(new()); private readonly Mock _prefill = new(); - private readonly Mock _profile = new(); + private readonly Mock _profile = new(); private readonly Mock _processEngine = new(); + private readonly Mock _oarganizationClientMock = new(); private InstancesController SUT => new InstancesController( _logger.Object, @@ -50,7 +57,8 @@ public class InstancesController_ActiveInstancesTest _appSettings, _prefill.Object, _profile.Object, - _processEngine.Object); + _processEngine.Object, + _oarganizationClientMock.Object); private void VerifyNoOtherCalls() { @@ -225,7 +233,7 @@ public async Task KnownUser_ReturnsUserName() LastChanged = i.LastChanged, LastChangedBy = i.LastChangedBy switch { - "12345" => "Ola Nordmann", + "12345" => "Ola Olsen", _ => throw new Exception("Unknown user"), } }); @@ -235,7 +243,7 @@ public async Task KnownUser_ReturnsUserName() { Party = new() { - Name = "Ola Nordmann" + Name = "Ola Olsen" } }); @@ -285,7 +293,7 @@ public async Task LastChangedBy9digits_LooksForOrg() }); _instanceClient.Setup(c => c.GetInstances(It.IsAny>())).ReturnsAsync(instances); - _registrer.Setup(r => r.ER.GetOrganization("123456789")).ReturnsAsync(default(Organization)); + _oarganizationClientMock.Setup(er => er.GetOrganization("123456789")).ReturnsAsync(default(Organization)); // Act var controller = SUT; @@ -299,7 +307,7 @@ public async Task LastChangedBy9digits_LooksForOrg() _instanceClient.Verify(c => c.GetInstances(It.Is>(query => query.ContainsKey("appId") ))); - _registrer.Verify(r => r.ER.GetOrganization("123456789")); + _oarganizationClientMock.Verify(er => er.GetOrganization("123456789")); VerifyNoOtherCalls(); } @@ -332,7 +340,7 @@ public async Task LastChangedBy9digits_FindsOrg() }); _instanceClient.Setup(c => c.GetInstances(It.IsAny>())).ReturnsAsync(instances); - _registrer.Setup(r => r.ER.GetOrganization("123456789")).ReturnsAsync(new Organization + _oarganizationClientMock.Setup(er => er.GetOrganization("123456789")).ReturnsAsync(new Organization { Name = "Testdepartementet" }); @@ -349,7 +357,7 @@ public async Task LastChangedBy9digits_FindsOrg() _instanceClient.Verify(c => c.GetInstances(It.Is>(query => query.ContainsKey("appId") ))); - _registrer.Verify(r => r.ER.GetOrganization("123456789")); + _oarganizationClientMock.Verify(er => er.GetOrganization("123456789")); VerifyNoOtherCalls(); } } diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs index a26bb125a..48c4e1741 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs @@ -3,10 +3,16 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Events; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Prefill; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.Validation; using Altinn.Authorization.ABAC.Xacml.JsonProfile; @@ -20,26 +26,28 @@ using Moq; using Xunit; +using IProcessEngine = Altinn.App.Core.Internal.Process.IProcessEngine; namespace Altinn.App.Api.Tests.Controllers; public class InstancesController_CopyInstanceTests { private readonly Mock> _logger = new(); - private readonly Mock _registrer = new(); - private readonly Mock _instanceClient = new(); - private readonly Mock _data = new(); + private readonly Mock _registrer = new(); + private readonly Mock _instanceClient = new(); + private readonly Mock _data = new(); private readonly Mock _appMetadata = new(); private readonly Mock _appModel = new(); private readonly Mock _instantiationProcessor = new(); private readonly Mock _instantiationValidator = new(); private readonly Mock _pdp = new(); - private readonly Mock _eventsService = new(); + private readonly Mock _eventsService = new(); private readonly IOptions _appSettings = Options.Create(new()); private readonly Mock _prefill = new(); - private readonly Mock _profile = new(); + private readonly Mock _profile = new(); private readonly Mock _processEngine = new(); private readonly Mock _httpContextMock = new(); + private readonly Mock _oarganizationClientMock = new(); private readonly InstancesController SUT; @@ -64,7 +72,8 @@ public InstancesController_CopyInstanceTests() _appSettings, _prefill.Object, _profile.Object, - _processEngine.Object) + _processEngine.Object, + _oarganizationClientMock.Object) { ControllerContext = controllerContext }; @@ -144,7 +153,7 @@ public async Task CopyInstance_AsUnauthorized_ReturnsForbidden() // Arrange const string Org = "ttd"; const string AppName = "copy-instance"; - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337)); + _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); _appMetadata.Setup(a => a.GetApplicationMetadata()) .ReturnsAsync(CreateApplicationMetadata($"{Org}/{AppName}", true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) @@ -177,7 +186,7 @@ public async Task CopyInstance_InstanceNotArchived_ReturnsBadRequest() Status = new InstanceStatus() { IsArchived = false } }; - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337)); + _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); _appMetadata.Setup(a => a.GetApplicationMetadata()) .ReturnsAsync(CreateApplicationMetadata($"{Org}/{AppName}", true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) @@ -212,7 +221,7 @@ public async Task CopyInstance_InstanceDoesNotExists_ReturnsBadRequest() PlatformHttpException platformHttpException = await PlatformHttpException.CreateAsync(new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)); - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337)); + _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); _appMetadata.Setup(a => a.GetApplicationMetadata()) .ReturnsAsync(CreateApplicationMetadata($"{Org}/{AppName}", true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) @@ -247,7 +256,7 @@ public async Task CopyInstance_PlatformReturnsError_ThrowsException() PlatformHttpException platformHttpException = await PlatformHttpException.CreateAsync(new HttpResponseMessage(System.Net.HttpStatusCode.BadGateway)); - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337)); + _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); _appMetadata.Setup(a => a.GetApplicationMetadata()) .ReturnsAsync(CreateApplicationMetadata($"{Org}/{AppName}", true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) @@ -291,7 +300,7 @@ public async Task CopyInstance_InstantiationValidationFails_ReturnsForbidden() }; InstantiationValidationResult? instantiationValidationResult = new() { Valid = false }; - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337)); + _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); _appMetadata.Setup(a => a.GetApplicationMetadata()) .ReturnsAsync(CreateApplicationMetadata($"{Org}/{AppName}", true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) @@ -338,7 +347,7 @@ public async Task CopyInstance_EverythingIsFine_ReturnsRedirect() }; InstantiationValidationResult? instantiationValidationResult = new() { Valid = true }; - _httpContextMock.Setup(hc => hc.User).Returns(PrincipalUtil.GetUserPrincipal(1337)); + _httpContextMock.Setup(hc => hc.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); _httpContextMock.Setup(hc => hc.Request).Returns(Mock.Of()); _appMetadata.Setup(a => a.GetApplicationMetadata()) .ReturnsAsync(CreateApplicationMetadata($"{Org}/{AppName}", true)); @@ -349,9 +358,9 @@ public async Task CopyInstance_EverythingIsFine_ReturnsRedirect() _instanceClient.Setup(i => i.CreateInstance(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(instance); _instanceClient.Setup(i => i.GetInstance(It.IsAny())).ReturnsAsync(instance); _instantiationValidator.Setup(v => v.Validate(It.IsAny())).ReturnsAsync(instantiationValidationResult); - _processEngine.Setup(p => p.StartProcess(It.IsAny())) - .ReturnsAsync((ProcessChangeContext pcc) => { return pcc; }); - _processEngine.Setup(p => p.StartTask(It.IsAny())); + _processEngine.Setup(p => p.StartProcess(It.IsAny())) + .ReturnsAsync(() => { return new ProcessChangeResult(){Success = true}; }); + _processEngine.Setup(p => p.UpdateInstanceAndRerunEvents(It.IsAny(), It.IsAny>())); // Act ActionResult actual = await SUT.CopyInstance(Org, AppName, InstanceOwnerPartyId, instanceGuid); diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs new file mode 100644 index 000000000..da7c28354 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs @@ -0,0 +1,119 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json; +using Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models; +using Altinn.App.Api.Tests.Utils; +using Altinn.App.Core.Features; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Altinn.App.Api.Tests.Controllers; + +public class InstancesController_PostNewInstanceTests : ApiTestBase, IClassFixture> +{ + private readonly Mock _dataProcessor = new(); + + private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + public InstancesController_PostNewInstanceTests(WebApplicationFactory factory) : base(factory) + { + OverrideServicesForAllTests = (services) => + { + services.AddSingleton(_dataProcessor.Object); + }; + } + [Fact] + public async Task PostNewInstanceWithContent_EnsureDataIsPresent() + { + // Setup test data + string testName = nameof(PostNewInstanceWithContent_EnsureDataIsPresent); + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 501337; + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetToken(1337, null); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Create instance data + using var content = new MultipartFormDataContent(); + content.Add(new StringContent($$$"""{{{testName}}}""", System.Text.Encoding.UTF8, "application/xml"), "default"); + + // Create instance + var createResponse = + await client.PostAsync($"{org}/{app}/instances/?instanceOwnerPartyId={instanceOwnerPartyId}", content); + var createResponseContent = await createResponse.Content.ReadAsStringAsync(); + createResponse.StatusCode.Should().Be(HttpStatusCode.Created, createResponseContent); + + var createResponseParsed = JsonSerializer.Deserialize(createResponseContent, JsonSerializerOptions)!; + + // Verify Data id + var instanceId = createResponseParsed.Id; + createResponseParsed.Data.Should().HaveCount(1, "Create instance should create a data element"); + var dataGuid = createResponseParsed.Data.First().Id; + + + // Verify stored data + var readDataElementResponse = await client.GetAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}"); + readDataElementResponse.StatusCode.Should().Be(HttpStatusCode.OK); + var readDataElementResponseContent = await readDataElementResponse.Content.ReadAsStringAsync(); + var readDataElementResponseParsed = + JsonSerializer.Deserialize(readDataElementResponseContent)!; + readDataElementResponseParsed.Melding.Name.Should().Be(testName); + } + + [Fact] + public async Task PostNewInstanceWithInvalidData_EnsureInvalidResponse() + { + // Should probably be BadRequest, but this is what the current implementation returns + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 501337; + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetToken(1337, null); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Create instance data + using var content = new MultipartFormDataContent(); + content.Add(new StringContent("INVALID XML", System.Text.Encoding.UTF8, "application/xml"), "default"); + + // Create instance + var createResponse = + await client.PostAsync($"{org}/{app}/instances/?instanceOwnerPartyId={instanceOwnerPartyId}", content); + var createResponseContent = await createResponse.Content.ReadAsStringAsync(); + createResponse.StatusCode.Should().Be(HttpStatusCode.InternalServerError, createResponseContent); + createResponseContent.Should().Contain("Instantiation of data elements failed"); + } + + + [Fact] + public async Task PostNewInstanceWithWrongPartname_EnsureBadRequest() + { + // Setup test data + string testName = nameof(PostNewInstanceWithWrongPartname_EnsureBadRequest); + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 501337; + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetToken(1337, null); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Create instance data + using var content = new MultipartFormDataContent(); + content.Add(new StringContent($$$"""{{{testName}}}""", System.Text.Encoding.UTF8, "application/xml"), "wrongName"); + + // Create instance + var createResponse = + await client.PostAsync($"{org}/{app}/instances/?instanceOwnerPartyId={instanceOwnerPartyId}", content); + var createResponseContent = await createResponse.Content.ReadAsStringAsync(); + createResponse.StatusCode.Should().Be(HttpStatusCode.BadRequest, createResponseContent); + createResponseContent.Should().Contain("Multipart section named, 'wrongName' does not correspond to an element"); + } +} diff --git a/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs index 259d82673..c5d2222bc 100644 --- a/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs @@ -5,6 +5,7 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Models; using FluentAssertions; +using Moq; namespace Altinn.App.Api.Tests.Controllers { @@ -26,8 +27,32 @@ public async Task Get_ShouldReturnParametersInHeader() string app = "contributer-restriction"; HttpClient client = GetRootedClient(org, app); + string url = $"/{org}/{app}/api/options/test?language=esperanto"; + HttpResponseMessage response = await client.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + response.StatusCode.Should().Be(HttpStatusCode.OK, content); + + var headerValue = response.Headers.GetValues("Altinn-DownstreamParameters"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + headerValue.Should().Contain("lang=esperanto"); + } + + [Fact] + public async Task Get_ShouldDefaultToNbLanguage() + { + OverrideServicesForThisTest = (services) => + { + services.AddTransient(); + }; + + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); + string url = $"/{org}/{app}/api/options/test"; HttpResponseMessage response = await client.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + response.StatusCode.Should().Be(HttpStatusCode.OK, content); var headerValue = response.Headers.GetValues("Altinn-DownstreamParameters"); response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -45,7 +70,7 @@ public Task GetAppOptionsAsync(string language, Dictionary() { - { "lang", "nb" } + { "lang", language } } }; diff --git a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs new file mode 100644 index 000000000..c646d0add --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs @@ -0,0 +1,96 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json; +using Altinn.App.Api.Models; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Api.Tests.Utils; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; + +namespace Altinn.App.Api.Tests.Controllers; + +public class ProcessControllerTests : ApiTestBase, IClassFixture> +{ + public ProcessControllerTests(WebApplicationFactory factory) : base(factory) + { + } + + [Fact] + public async Task Get_ShouldReturnProcessTasks() + { + string org = "tdd"; + string app = "contributer-restriction"; + int partyId = 500000; + Guid instanceId = new Guid("5d9e906b-83ed-44df-85a7-2f104c640bff"); + HttpClient client = GetRootedClient(org, app); + + TestData.DeleteInstance(org, app, partyId, instanceId); + TestData.PrepareInstance(org, app, partyId, instanceId); + + string token = PrincipalUtil.GetToken(1337, 500000, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + string url = $"/{org}/{app}/instances/{partyId}/{instanceId}/process"; + HttpResponseMessage response = await client.GetAsync(url); + TestData.DeleteInstance(org, app, partyId, instanceId); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var content = await response.Content.ReadAsStringAsync(); + var expectedString = """ + { + "currentTask": { + "actions": { + "read": true, + "write": true + }, + "userActions": [ + { + "id": "read", + "authorized": true, + "type": "ProcessAction" + }, + { + "id": "write", + "authorized": true, + "type": "ProcessAction" + } + ], + "read": true, + "write": true, + "flow": 2, + "started": "2019-12-05T13:24:34.9196661Z", + "elementId": "Task_1", + "name": "Utfylling", + "altinnTaskType": "data", + "ended": null, + "validated": { + "timestamp": "2020-02-07T10:46:36.985894Z", + "canCompleteTask": false + }, + "flowType": null + }, + "processTasks": [ + { + "altinnTaskType": "data", + "elementId": "Task_1" + } + ], + "started": "2019-12-05T13:24:34.8412179Z", + "startEvent": "StartEvent_1", + "ended": null, + "endEvent": null + } + """; + CompareResult(expectedString, content); + } + + + //TODO: replace this assertion with a proper one once fluentassertions has a json compare feature scheduled for v7 https://github.com/fluentassertions/fluentassertions/issues/2205 + private static void CompareResult(string expectedString, string actualString) + { + T? expected = JsonSerializer.Deserialize(expectedString); + T? actual = JsonSerializer.Deserialize(actualString); + actual.Should().BeEquivalentTo(expected); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs index 88bbb9076..e22b16245 100644 --- a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs @@ -1,14 +1,14 @@ -using System.Collections.Generic; using System.Net.Http.Headers; using System.Security.Claims; using Altinn.App.Api.Controllers; using Altinn.App.Api.Tests.Controllers.TestResources; using Altinn.App.Api.Tests.Utils; using Altinn.App.Core.Features; -using Altinn.App.Core.Features.DataProcessing; -using Altinn.App.Core.Infrastructure.Clients.Profile; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Prefill; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.Authorization.ABAC.Xacml; using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Interfaces; @@ -36,11 +36,11 @@ public async void Get_Returns_BadRequest_when_dataType_is_null() var appResourcesMock = new Mock(); var dataProcessorMock = new Mock(); var prefillMock = new Mock(); - var registerMock = new Mock(); + var registerMock = new Mock(); var pdpMock = new Mock(); ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController(logger, altinnAppModelMock.Object, appResourcesMock.Object, - dataProcessorMock.Object, prefillMock.Object, registerMock.Object, pdpMock.Object); + prefillMock.Object, registerMock.Object, pdpMock.Object, new IDataProcessor[] { dataProcessorMock.Object }); string dataType = null!; // this is what we're testing @@ -65,12 +65,12 @@ public async void Get_Returns_BadRequest_when_appResource_classRef_is_null() var appResourcesMock = new Mock(); var dataProcessorMock = new Mock(); var prefillMock = new Mock(); - var registerMock = new Mock(); + var registerMock = new Mock(); var pdpMock = new Mock(); var dataType = "some-value"; ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController(logger, appModelMock.Object, appResourcesMock.Object, - dataProcessorMock.Object, prefillMock.Object, registerMock.Object, pdpMock.Object); + prefillMock.Object, registerMock.Object, pdpMock.Object, new IDataProcessor[] { dataProcessorMock.Object }); // Act @@ -92,8 +92,8 @@ public async void Get_Returns_BadRequest_when_appResource_classRef_is_null() // party headers. private class StatelessDataControllerWebApplicationFactory : WebApplicationFactory { - public Mock ProfileClientMoq { get; set; } = new(); - public Mock RegisterClientMoq { get; set; } = new(); + public Mock ProfileClientMoq { get; set; } = new(); + public Mock RegisterClientMoq { get; set; } = new(); public Mock AppResourcesMoq { get; set; } = new(); protected override void ConfigureWebHost(IWebHostBuilder builder) @@ -102,8 +102,8 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) builder.ConfigureServices(services=> { - services.AddTransient((sp)=>ProfileClientMoq.Object); - services.AddTransient((sp)=>RegisterClientMoq.Object); + services.AddTransient((sp)=>ProfileClientMoq.Object); + services.AddTransient((sp)=>RegisterClientMoq.Object); services.AddTransient((sp)=>AppResourcesMoq.Object); }); } @@ -116,7 +116,7 @@ public async void Get_Returns_BadRequest_when_party_header_count_greater_than_on var factory = new StatelessDataControllerWebApplicationFactory(); var client = factory.CreateClient(); - string token = PrincipalUtil.GetToken(1337); + string token = PrincipalUtil.GetToken(1337, null); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var request = new HttpRequestMessage(HttpMethod.Get, "/tdd/demo-app/v1/data?dataType=xml"); request.Headers.Add("party", new string[]{"partyid:234", "partyid:234"}); // Double header @@ -143,7 +143,7 @@ public async void Get_Returns_Forbidden_when_party_has_no_rights() var factory = new StatelessDataControllerWebApplicationFactory(); var client = factory.CreateClient(); - string token = PrincipalUtil.GetToken(1337); + string token = PrincipalUtil.GetToken(1337, null); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var request = new HttpRequestMessage(HttpMethod.Get, "/tdd/demo-app/v1/data?dataType=xml"); request.Headers.Add("party", new string[]{"partyid:234"}); @@ -170,12 +170,12 @@ public async void Get_Returns_BadRequest_when_instance_owner_is_empty_party_head var appResourcesMock = new Mock(); var dataProcessorMock = new Mock(); var prefillMock = new Mock(); - var registerMock = new Mock(); + var registerMock = new Mock(); var pdpMock = new Mock(); var dataType = "some-value"; ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController(logger, appModelMock.Object, appResourcesMock.Object, - dataProcessorMock.Object, prefillMock.Object, registerMock.Object, pdpMock.Object); + prefillMock.Object, registerMock.Object, pdpMock.Object, new IDataProcessor[] { dataProcessorMock.Object }); // Act appResourcesMock.Setup(x => x.GetClassRefForLogicDataType(dataType)).Returns(typeof(DummyModel).FullName!); @@ -201,12 +201,12 @@ public async void Get_Returns_BadRequest_when_instance_owner_is_empty_user_in_co var appResourcesMock = new Mock(); var dataProcessorMock = new Mock(); var prefillMock = new Mock(); - var registerMock = new Mock(); + var registerMock = new Mock(); var pdpMock = new Mock(); var dataType = "some-value"; ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController(logger, appModelMock.Object, appResourcesMock.Object, - dataProcessorMock.Object, prefillMock.Object, registerMock.Object, pdpMock.Object); + prefillMock.Object, registerMock.Object, pdpMock.Object, new IDataProcessor[] { dataProcessorMock.Object }); statelessDataController.ControllerContext = new ControllerContext(); statelessDataController.ControllerContext.HttpContext = new DefaultHttpContext(); statelessDataController.ControllerContext.HttpContext.User = new ClaimsPrincipal(new List() @@ -241,12 +241,12 @@ public async void Get_Returns_Forbidden_when_returned_descision_is_Deny() var appResourcesMock = new Mock(); var dataProcessorMock = new Mock(); var prefillMock = new Mock(); - var registerMock = new Mock(); + var registerMock = new Mock(); var pdpMock = new Mock(); var dataType = "some-value"; ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController(logger, appModelMock.Object, appResourcesMock.Object, - dataProcessorMock.Object, prefillMock.Object, registerMock.Object, pdpMock.Object); + prefillMock.Object, registerMock.Object, pdpMock.Object, new IDataProcessor[] { dataProcessorMock.Object }); statelessDataController.ControllerContext = new ControllerContext(); statelessDataController.ControllerContext.HttpContext = new DefaultHttpContext(); statelessDataController.ControllerContext.HttpContext.User = new ClaimsPrincipal(new List() @@ -298,13 +298,13 @@ public async void Get_Returns_OK_with_appModel() var appResourcesMock = new Mock(); var dataProcessorMock = new Mock(); var prefillMock = new Mock(); - var registerMock = new Mock(); + var registerMock = new Mock(); var pdpMock = new Mock(); var dataType = "some-value"; var classRef = typeof(DummyModel).FullName!; ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController(logger, appModelMock.Object, appResourcesMock.Object, - dataProcessorMock.Object, prefillMock.Object, registerMock.Object, pdpMock.Object); + prefillMock.Object, registerMock.Object, pdpMock.Object, new IDataProcessor[] { dataProcessorMock.Object }); statelessDataController.ControllerContext = new ControllerContext(); statelessDataController.ControllerContext.HttpContext = new DefaultHttpContext(); statelessDataController.ControllerContext.HttpContext.User = new ClaimsPrincipal(new List() diff --git a/test/Altinn.App.Api.Tests/Controllers/StatelessPagesControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/StatelessPagesControllerTests.cs index c4d8ff8eb..b62d67387 100644 --- a/test/Altinn.App.Api.Tests/Controllers/StatelessPagesControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/StatelessPagesControllerTests.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Altinn.App.Api.Controllers; using Altinn.App.Api.Tests.Controllers.TestResources; using Altinn.App.Core.Features; -using Altinn.App.Core.Features.PageOrder; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; using Altinn.App.Core.Models; using FluentAssertions; diff --git a/test/Altinn.App.Api.Tests/Controllers/TextsControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/TextsControllerTests.cs index 59fff3a09..01d5482d7 100644 --- a/test/Altinn.App.Api.Tests/Controllers/TextsControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/TextsControllerTests.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; using Altinn.App.Api.Controllers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Microsoft.AspNetCore.Mvc; diff --git a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs index fedb48e3b..75e56fcfa 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs @@ -1,9 +1,10 @@ using System.Net; using Altinn.App.Api.Controllers; +using Altinn.App.Core.Features; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; @@ -19,9 +20,9 @@ public class ValidateControllerTests public async Task ValidateInstance_returns_NotFound_when_GetInstance_returns_null() { // Arrange - var instanceMock = new Mock(); + var instanceMock = new Mock(); var appMetadataMock = new Mock(); - var validationMock = new Mock(); + var validationMock = new Mock(); const string org = "ttd"; const string app = "app"; @@ -43,9 +44,9 @@ public async Task ValidateInstance_returns_NotFound_when_GetInstance_returns_nul public async Task ValidateInstance_throws_ValidationException_when_Instance_Process_is_null() { // Arrange - var instanceMock = new Mock(); + var instanceMock = new Mock(); var appMetadataMock = new Mock(); - var validationMock = new Mock(); + var validationMock = new Mock(); const string org = "ttd"; const string app = "app"; @@ -75,9 +76,9 @@ public async Task ValidateInstance_throws_ValidationException_when_Instance_Proc public async Task ValidateInstance_throws_ValidationException_when_Instance_Process_CurrentTask_is_null() { // Arrange - var instanceMock = new Mock(); + var instanceMock = new Mock(); var appMetadataMock = new Mock(); - var validationMock = new Mock(); + var validationMock = new Mock(); const string org = "ttd"; const string app = "app"; @@ -110,9 +111,9 @@ public async Task ValidateInstance_throws_ValidationException_when_Instance_Proc public async Task ValidateInstance_returns_OK_with_messages() { // Arrange - var instanceMock = new Mock(); + var instanceMock = new Mock(); var appMetadataMock = new Mock(); - var validationMock = new Mock(); + var validationMock = new Mock(); const string org = "ttd"; const string app = "app"; @@ -143,7 +144,7 @@ public async Task ValidateInstance_returns_OK_with_messages() instanceMock.Setup(i => i.GetInstance(app, org, instanceOwnerPartyId, instanceId)) .Returns(Task.FromResult(instance)); - validationMock.Setup(v => v.ValidateAndUpdateProcess(instance, "dummy")) + validationMock.Setup(v => v.ValidateInstanceAtTask(instance, "dummy")) .Returns(Task.FromResult(validationResult)); // Act @@ -159,9 +160,9 @@ public async Task ValidateInstance_returns_OK_with_messages() public async Task ValidateInstance_returns_403_when_not_authorized() { // Arrange - var instanceMock = new Mock(); + var instanceMock = new Mock(); var appMetadataMock = new Mock(); - var validationMock = new Mock(); + var validationMock = new Mock(); const string org = "ttd"; const string app = "app"; @@ -186,7 +187,7 @@ public async Task ValidateInstance_returns_403_when_not_authorized() instanceMock.Setup(i => i.GetInstance(app, org, instanceOwnerPartyId, instanceId)) .Returns(Task.FromResult(instance)); - validationMock.Setup(v => v.ValidateAndUpdateProcess(instance, "dummy")) + validationMock.Setup(v => v.ValidateInstanceAtTask(instance, "dummy")) .Throws(exception); // Act @@ -202,9 +203,9 @@ public async Task ValidateInstance_returns_403_when_not_authorized() public async Task ValidateInstance_throws_PlatformHttpException_when_not_403() { // Arrange - var instanceMock = new Mock(); + var instanceMock = new Mock(); var appMetadataMock = new Mock(); - var validationMock = new Mock(); + var validationMock = new Mock(); const string org = "ttd"; const string app = "app"; @@ -229,7 +230,7 @@ public async Task ValidateInstance_throws_PlatformHttpException_when_not_403() instanceMock.Setup(i => i.GetInstance(app, org, instanceOwnerPartyId, instanceId)) .Returns(Task.FromResult(instance)); - validationMock.Setup(v => v.ValidateAndUpdateProcess(instance, "dummy")) + validationMock.Setup(v => v.ValidateInstanceAtTask(instance, "dummy")) .Throws(exception); // Act diff --git a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs index 2e6035a1c..b09672457 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs @@ -1,13 +1,16 @@ using System.Collections; using Altinn.App.Api.Controllers; +using Altinn.App.Core.Features; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Infrastructure.Clients; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Mvc; +using FluentAssertions; using Moq; using Xunit; @@ -122,7 +125,6 @@ public class TestScenariosData : IEnumerable new ValidationIssue { Code = ValidationIssueCodes.DataElementCodes.DataElementValidatedAtWrongTask, - InstanceId = "0fc98a23-fe31-4ef5-8fb9-dd3f479354ef", Severity = ValidationIssueSeverity.Warning, DataElementId = "0fc98a23-fe31-4ef5-8fb9-dd3f479354cd", Description = AppTextHelper.GetAppText( @@ -220,7 +222,7 @@ public async Task TestValidateData(ValidateDataTestScenario testScenario) { var result = await validateController.ValidateData(org, app, instanceOwnerId, testScenario.InstanceId, testScenario.DataGuid); - Assert.IsType(testScenario.ExpectedResult, result); + result.Should().BeOfType(testScenario.ExpectedResult); } else { @@ -234,18 +236,18 @@ public async Task TestValidateData(ValidateDataTestScenario testScenario) private static ValidateController SetupController(string app, string org, int instanceOwnerId, ValidateDataTestScenario testScenario) { - (Mock instanceMock, Mock appResourceMock, Mock validationMock) = + (Mock instanceMock, Mock appResourceMock, Mock validationMock) = SetupMocks(app, org, instanceOwnerId, testScenario); return new ValidateController(instanceMock.Object, validationMock.Object, appResourceMock.Object); } - private static (Mock, Mock, Mock) SetupMocks(string app, string org, + private static (Mock, Mock, Mock) SetupMocks(string app, string org, int instanceOwnerId, ValidateDataTestScenario testScenario) { - var instanceMock = new Mock(); + var instanceMock = new Mock(); var appMetadataMock = new Mock(); - var validationMock = new Mock(); + var validationMock = new Mock(); if (testScenario.ReceivedInstance != null) { instanceMock.Setup(i => i.GetInstance(app, org, instanceOwnerId, testScenario.InstanceId)) @@ -261,8 +263,8 @@ private static (Mock, Mock, Mock) SetupMoc { validationMock.Setup(v => v.ValidateDataElement( testScenario.ReceivedInstance, - testScenario.ReceivedApplication.DataTypes.First(), - testScenario.ReceivedInstance.Data.First())) + testScenario.ReceivedInstance.Data.First(), + testScenario.ReceivedApplication.DataTypes.First())) .Returns(Task.FromResult>(testScenario.ReceivedValidationIssues)); } diff --git a/test/Altinn.App.Api.Tests/Controllers/ValidateController_ValidateInstanceTests.cs b/test/Altinn.App.Api.Tests/Controllers/ValidateController_ValidateInstanceTests.cs new file mode 100644 index 000000000..aedca21c9 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/ValidateController_ValidateInstanceTests.cs @@ -0,0 +1,138 @@ +using Altinn.App.Api.Tests.Utils; +using Microsoft.AspNetCore.Mvc.Testing; +using System.Net.Http.Headers; +using System.Net; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Altinn.App.Api.Models; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models; +using Altinn.App.Core.Features; +using Altinn.App.Core.Models.Validation; +using Xunit; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Json.Patch; +using Json.Pointer; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit.Abstractions; + +namespace Altinn.App.Api.Tests.Controllers; + +public class ValidateControllerValidateInstanceTests : ApiTestBase, IClassFixture> +{ + private const string Org = "tdd"; + private const string App = "contributer-restriction"; + private const int InstanceOwnerPartyId = 500600; + private static readonly Guid InstanceGuid = new("3102f61d-1446-4ca5-9fed-3c7c7d67249c"); + private static readonly string InstanceId = $"{InstanceOwnerPartyId}/{InstanceGuid}"; + private static readonly Guid DataGuid = new("5240d834-dca6-44d3-b99a-1b7ca9b862af"); + + private readonly Mock _dataProcessorMock = new(); + private readonly Mock _formDataValidatorMock = new(); + + private static readonly JsonSerializerOptions JsonSerializerOptions = new () + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + UnknownTypeHandling = JsonUnknownTypeHandling.JsonElement, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; + + private readonly ITestOutputHelper _outputHelper; + + public ValidateControllerValidateInstanceTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) : base(factory) + { + _formDataValidatorMock.Setup(v => v.DataType).Returns("Not a valid data type"); + _outputHelper = outputHelper; + OverrideServicesForAllTests = (services) => + { + services.AddSingleton(_dataProcessorMock.Object); + services.AddSingleton(_formDataValidatorMock.Object); + }; + TestData.DeleteInstanceAndData(Org, App, InstanceOwnerPartyId, InstanceGuid); + TestData.PrepareInstance(Org, App, InstanceOwnerPartyId, InstanceGuid); + } + + private async Task CallValidateInstanceApi() + { + using var httpClient = GetRootedClient(Org, App); + string token = PrincipalUtil.GetToken(1337, null); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + return await httpClient.GetAsync($"/{Org}/{App}/instances/{InstanceId}/validate"); + } + + private async Task<(HttpResponseMessage response, string responseString)> CallValidateDataApi() + { + using var httpClient = GetRootedClient(Org, App); + string token = PrincipalUtil.GetToken(1337, null); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + var response = await httpClient.GetAsync($"/{Org}/{App}/instances/{InstanceId}/data/{DataGuid}/validate"); + var responseString = await LogResponse(response); + return (response, responseString); + } + + private async Task LogResponse(HttpResponseMessage response) + { + var responseString = await response.Content.ReadAsStringAsync(); + using var responseParsedRaw = JsonDocument.Parse(responseString); + _outputHelper.WriteLine(JsonSerializer.Serialize(responseParsedRaw, JsonSerializerOptions)); + return responseString; + + } + private static TResponse ParseResponse(string responseString) + { + return JsonSerializer.Deserialize(responseString, JsonSerializerOptions)!; + } + + [Fact] + public async Task ValidateInstance_NoSetup() + { + var response = await CallValidateInstanceApi(); + var responseString = await LogResponse(response); + + response.Should().HaveStatusCode(HttpStatusCode.OK); + var parsedResponse = ParseResponse>(responseString); + parsedResponse.Should().BeEmpty(); + + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ValidateInstance_WithTaskValidator() + { + var oldTaskValidatorMock = new Mock(MockBehavior.Strict); + + oldTaskValidatorMock.Setup(v => v.ValidateTask(It.IsAny(), "Task_1", It.IsAny())) + .Returns( + (Instance instance, string task, ModelStateDictionary issues) => + { + issues.AddModelError((Skjema s)=>s.Melding.NestedList, "CustomErrorText"); + return Task.CompletedTask; + }).Verifiable(Times.Once); + + OverrideServicesForThisTest = (services) => + { + services.AddSingleton(oldTaskValidatorMock.Object); + }; + var response = await CallValidateInstanceApi(); + var responseString = await LogResponse(response); + + response.Should().HaveStatusCode(HttpStatusCode.OK); + var parsedResponse = ParseResponse>(responseString); + var singleIssue = parsedResponse.Should().ContainSingle().Which; + singleIssue.Field.Should().BeNull(); + singleIssue.Code.Should().Be("CustomErrorText"); + singleIssue.Severity.Should().Be(ValidationIssueSeverity.Error); + + _dataProcessorMock.VerifyNoOtherCalls(); + oldTaskValidatorMock.Verify(); + oldTaskValidatorMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs b/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs index 239ec84fd..5ad78fa26 100644 --- a/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs +++ b/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs @@ -37,7 +37,7 @@ public HttpClient GetRootedClient(string org, string app) builder.ConfigureServices(services => services.Configure(appSettingSection)); builder.ConfigureTestServices(services => OverrideServicesForAllTests(services)); builder.ConfigureTestServices(OverrideServicesForThisTest); - }).CreateClient(); + }).CreateClient(new WebApplicationFactoryClientOptions() { AllowAutoRedirect = false }); return client; } diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/.gitignore b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/.gitignore new file mode 100644 index 000000000..dcf6ac329 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/.gitignore @@ -0,0 +1,4 @@ +# Ignore guid.json files +????????-????-????-????-????????????.json +# ignore copied blobs +*/*/blob/????????-????-????-????-???????????? \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500000/5d9e906b-83ed-44df-85a7-2f104c640bff.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500000/5d9e906b-83ed-44df-85a7-2f104c640bff.pretest.json new file mode 100644 index 000000000..407a73f44 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500000/5d9e906b-83ed-44df-85a7-2f104c640bff.pretest.json @@ -0,0 +1,39 @@ +{ + "id": "500000/5d9e906b-83ed-44df-85a7-2f104c640bff", + "instanceOwner": { + "partyId": "500000", + "personNumber": "01039012345" + }, + "appId": "tdd/contributer-restriction", + "org": "tdd", + "process": { + "started": "2019-12-05T13:24:34.8412179Z", + "startEvent": "StartEvent_1", + "currentTask": { + "flow": 2, + "started": "2019-12-05T13:24:34.9196661Z", + "elementId": "Task_1", + "name": "Utfylling", + "altinnTaskType": "data", + "validated": { + "timestamp": "2020-02-07T10:46:36.985894Z", + "canCompleteTask": false + } + } + }, + "status": { + "isArchived": false, + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + }, + "data": [ + { + "id": "de288942-a8af-4f77-a1f1-6e1ede1cd502", + "dataType": "default", + "contentType": "application/xml", + "size": 0, + "locked": false + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd.pretest.json new file mode 100644 index 000000000..1cc0c5045 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd.pretest.json @@ -0,0 +1,30 @@ +{ + "id": "500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd", + "instanceOwner": { + "partyId": "500600", + "organisationNumber": "897069631" + }, + "appId": "tdd/contributer-restriction", + "org": "tdd", + "process": { + "started": "2019-12-05T13:24:34.8412179Z", + "startEvent": "StartEvent_1", + "currentTask": { + "flow": 2, + "started": "2019-12-05T13:24:34.9196661Z", + "elementId": "Task_1", + "name": "Utfylling", + "altinnTaskType": "data", + "validated": { + "timestamp": "2020-02-07T10:46:36.985894Z", + "canCompleteTask": false + } + } + }, + "status": { + "isArchived": false, + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd/blob/fc121812-0336-45fb-a75c-490df3ad5109.pretest b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd/blob/fc121812-0336-45fb-a75c-490df3ad5109.pretest new file mode 100644 index 000000000..903c3e28a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd/blob/fc121812-0336-45fb-a75c-490df3ad5109.pretest @@ -0,0 +1,6 @@ + + + + false + + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd/fc121812-0336-45fb-a75c-490df3ad5109.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd/fc121812-0336-45fb-a75c-490df3ad5109.pretest.json new file mode 100644 index 000000000..bc6bc73e5 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd/fc121812-0336-45fb-a75c-490df3ad5109.pretest.json @@ -0,0 +1,22 @@ +{ + "id": "fc121812-0336-45fb-a75c-490df3ad5109", + "instanceGuid": "0fc98a23-fe31-4ef5-8fb9-dd3f479354cd", + "dataType": "default", + "filename": null, + "contentType": "application/xml", + "blobStoragePath": null, + "selfLinks": null, + "size": 0, + "contentHash": null, + "locked": false, + "refs": null, + "isRead": true, + "tags": [], + "deleteStatus": null, + "fileScanResult": "NotApplicable", + "references": null, + "created": null, + "createdBy": null, + "lastChanged": "2024-01-10T22:04:31.511965Z", + "lastChangedBy": null +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c.pretest.json new file mode 100644 index 000000000..5b305331b --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c.pretest.json @@ -0,0 +1,30 @@ +{ + "id": "500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c", + "instanceOwner": { + "partyId": "500600", + "organisationNumber": "897069631" + }, + "appId": "tdd/contributer-restriction", + "org": "tdd", + "process": { + "started": "2019-12-05T13:24:34.8412179Z", + "startEvent": "StartEvent_1", + "currentTask": { + "flow": 2, + "started": "2019-12-05T13:24:34.9196661Z", + "elementId": "Task_1", + "name": "Utfylling", + "altinnTaskType": "data", + "validated": { + "timestamp": "2020-02-07T10:46:36.985894Z", + "canCompleteTask": false + } + } + }, + "status": { + "isArchived": false, + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c/5240d834-dca6-44d3-b99a-1b7ca9b862af.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c/5240d834-dca6-44d3-b99a-1b7ca9b862af.pretest.json new file mode 100644 index 000000000..c63a52a95 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c/5240d834-dca6-44d3-b99a-1b7ca9b862af.pretest.json @@ -0,0 +1,22 @@ +{ + "id": "5240d834-dca6-44d3-b99a-1b7ca9b862af", + "instanceGuid": "3102f61d-1446-4ca5-9fed-3c7c7d67249c", + "dataType": "default", + "filename": null, + "contentType": "application/xml", + "blobStoragePath": null, + "selfLinks": null, + "size": 0, + "contentHash": null, + "locked": false, + "refs": null, + "isRead": true, + "tags": [], + "deleteStatus": null, + "fileScanResult": "NotApplicable", + "references": null, + "created": null, + "createdBy": null, + "lastChanged": "2024-01-10T22:04:31.511965Z", + "lastChangedBy": null +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c/blob/5240d834-dca6-44d3-b99a-1b7ca9b862af.pretest b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c/blob/5240d834-dca6-44d3-b99a-1b7ca9b862af.pretest new file mode 100644 index 000000000..2f4a2b54a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c/blob/5240d834-dca6-44d3-b99a-1b7ca9b862af.pretest @@ -0,0 +1,7 @@ + + + + Per Olsen + false + + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef41.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef41.pretest.json new file mode 100644 index 000000000..69ac2d877 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef41.pretest.json @@ -0,0 +1,39 @@ +{ + "id": "1337/b1135209-628e-4a6e-9efd-e4282068ef41", + "instanceOwner": { + "partyId": "1337", + "personNumber": "01039012345" + }, + "appId": "tdd/task-action", + "org": "tdd", + "process": { + "started": "2019-12-05T13:24:34.8412179Z", + "startEvent": "StartEvent_1", + "currentTask": { + "flow": 2, + "started": "2019-12-05T13:24:34.9196661Z", + "elementId": "Task_1", + "name": "Utfylling", + "altinnTaskType": "data", + "validated": { + "timestamp": "2020-02-07T10:46:36.985894+01:00", + "canCompleteTask": false + } + } + }, + "status": { + "isArchived": false, + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + }, + "data": [ + { + "id": "de288942-a8af-4f77-a1f1-6e1ede1cd502", + "dataType": "default", + "contentType": "application/xml", + "size": 0, + "locked": false + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef42.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef42.pretest.json new file mode 100644 index 000000000..e0c090e9c --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef42.pretest.json @@ -0,0 +1,31 @@ +{ + "id": "1337/b1135209-628e-4a6e-9efd-e4282068ef41", + "instanceOwner": { + "partyId": "1337", + "personNumber": "01039012345" + }, + "appId": "tdd/task-action", + "org": "tdd", + "process": { + "started": "2023-11-15T09:47:36.21031Z", + "startEvent": "StartEvent_1", + "ended": "2023-11-15T09:47:39.979157Z", + "endEvent": "EndEvent_1" + }, + "status": { + "isArchived": true, + "archived": "2023-11-15T09:47:39.979157Z", + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + }, + "data": [ + { + "id": "de288942-a8af-4f77-a1f1-6e1ede1cd502", + "dataType": "default", + "contentType": "application/xml", + "size": 0, + "locked": false + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef43.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef43.pretest.json new file mode 100644 index 000000000..6b17a70bf --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef43.pretest.json @@ -0,0 +1,26 @@ +{ + "id": "1337/b1135209-628e-4a6e-9efd-e4282068ef41", + "instanceOwner": { + "partyId": "1337", + "personNumber": "01039012345" + }, + "appId": "tdd/task-action", + "org": "tdd", + "process": null, + "status": { + "isArchived": true, + "archived": "2023-11-15T09:47:39.979157Z", + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + }, + "data": [ + { + "id": "de288942-a8af-4f77-a1f1-6e1ede1cd502", + "dataType": "default", + "contentType": "application/xml", + "size": 0, + "locked": false + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Data/Profile/User/1001.json b/test/Altinn.App.Api.Tests/Data/Profile/User/1001.json new file mode 100644 index 000000000..4338605a9 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Profile/User/1001.json @@ -0,0 +1,13 @@ +{ + "UserId": 1001, + "UserName": "PengelensPartner", + "PhoneNumber": "12345678", + "Email": "test@test.com", + "PartyId": 510001, + "Party": {}, + "UserType": 0, + "ProfileSettingPreference": { + "Language": "nb", + "doNotPromptForParty": true + } +} diff --git a/test/Altinn.App.Api.Tests/Data/Profile/User/1002.json b/test/Altinn.App.Api.Tests/Data/Profile/User/1002.json new file mode 100644 index 000000000..a7767b65d --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Profile/User/1002.json @@ -0,0 +1,13 @@ +{ + "UserId": 1002, + "UserName": "GjentagendeForelder", + "PhoneNumber": "12345678", + "Email": "test@test.com", + "PartyId": 510002, + "Party": {}, + "UserType": 0, + "ProfileSettingPreference": { + "Language": "nb", + "doNotPromptForParty": true + } +} diff --git a/test/Altinn.App.Api.Tests/Data/Profile/User/1003.json b/test/Altinn.App.Api.Tests/Data/Profile/User/1003.json new file mode 100644 index 000000000..b04e2df1d --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Profile/User/1003.json @@ -0,0 +1,13 @@ +{ + "UserId": 1003, + "UserName": "RikForelder", + "PhoneNumber": "12345678", + "Email": "test@test.com", + "PartyId": 510003, + "Party": {}, + "UserType": 0, + "ProfileSettingPreference": { + "Language": "nb", + "doNotPromptForParty": true + } +} diff --git a/test/Altinn.App.Api.Tests/Data/Profile/User/12345.json b/test/Altinn.App.Api.Tests/Data/Profile/User/12345.json new file mode 100644 index 000000000..d1ad5db12 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Profile/User/12345.json @@ -0,0 +1,13 @@ +{ + "UserId": 12345, + "UserName": "OlaNordmann", + "PhoneNumber": "12345678", + "Email": "test@test.com", + "PartyId": 512345, + "Party": {}, + "UserType": 0, + "ProfileSettingPreference": { + "Language": "nb", + "doNotPromptForParty": true + } +} diff --git a/test/Altinn.App.Api.Tests/Data/Profile/User/1337.json b/test/Altinn.App.Api.Tests/Data/Profile/User/1337.json new file mode 100644 index 000000000..7624c62be --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Profile/User/1337.json @@ -0,0 +1,13 @@ +{ + "UserId": 1337, + "UserName": "SophieDDG", + "PhoneNumber": "90001337", + "Email": "1337@altinnstudiotestusers.com", + "PartyId": 501337, + "Party": {}, + "UserType": 1, + "ProfileSettingPreference": { + "Language": "nn", + "doNotPromptForParty": true + } +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/897069631.json b/test/Altinn.App.Api.Tests/Data/Register/Org/897069631.json new file mode 100644 index 000000000..e4cc66b51 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/897069631.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "897069631", + "Name": "EAS Health Consulting", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010000", + "FaxNumber": "92110000", + "EMailAddress": "epost@setra.no", + "InternetAddress": "http://setrabrl.no", + "MailingAddress": "Sofies Gate 2", + "MailingPostalCode": "0170", + "MailingPostalCity": "Oslo", + "BusinessAddress": "Sofies Gate 2", + "BusinessPostalCode": "0170", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/897069650.json b/test/Altinn.App.Api.Tests/Data/Register/Org/897069650.json new file mode 100644 index 000000000..8c0fc65a7 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/897069650.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "897069650", + "Name": "DDG Fitness", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010000", + "FaxNumber": "92110000", + "EMailAddress": "central@ddgfitness.no", + "InternetAddress": "http://ddgfitness.no", + "MailingAddress": "Sofies Gate 1", + "MailingPostalCode": "0170", + "MailingPostalCity": "Oslo", + "BusinessAddress": "Sofies Gate 1", + "BusinessPostalCode": "0170", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/897069651.json b/test/Altinn.App.Api.Tests/Data/Register/Org/897069651.json new file mode 100644 index 000000000..ee5880e56 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/897069651.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "897069651", + "Name": "DDG Fitness Oslo", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010001", + "FaxNumber": "92110001", + "EMailAddress": "oslo@ddgfitness.no", + "InternetAddress": "http://ddgfitness.no", + "MailingAddress": "Sofies Gate 1", + "MailingPostalCode": "0170", + "MailingPostalCity": "Oslo", + "BusinessAddress": "Sofies Gate 1", + "BusinessPostalCode": "0170", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/897069652.json b/test/Altinn.App.Api.Tests/Data/Register/Org/897069652.json new file mode 100644 index 000000000..2e0c75d63 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/897069652.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "897069652", + "Name": "DDG Fitness Bergen", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010002", + "FaxNumber": "92110002", + "EMailAddress": "bergen@ddgfitness.no", + "InternetAddress": "http://ddgfitness.no", + "MailingAddress": "Olav Kyrres Gate 11", + "MailingPostalCode": "5014", + "MailingPostalCity": "Bergen", + "BusinessAddress": "Olav Kyrres Gate 11", + "BusinessPostalCode": "5014", + "BusinessPostalCity": "Bergen" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/897069653.json b/test/Altinn.App.Api.Tests/Data/Register/Org/897069653.json new file mode 100644 index 000000000..787630812 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/897069653.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "897069653", + "Name": "DDG Fitness Trondheim", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010003", + "FaxNumber": "92110003", + "EMailAddress": "trondheim@ddgfitness.no", + "InternetAddress": "http://ddgfitness.no", + "MailingAddress": "Kjøpmannsgata 25", + "MailingPostalCode": "7013", + "MailingPostalCity": "Trondheim", + "BusinessAddress": "Kjøpmannsgata 25", + "BusinessPostalCode": "7013", + "BusinessPostalCity": "Trondheim" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/900000001.json b/test/Altinn.App.Api.Tests/Data/Register/Org/900000001.json new file mode 100644 index 000000000..717083744 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/900000001.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "900000001", + "Name": "Kari Consulting", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "1234578", + "FaxNumber": "12345678", + "EMailAddress": "email@email.com", + "InternetAddress": "http://example.com", + "MailingAddress": "Postadresse 9", + "MailingPostalCode": "0000", + "MailingPostalCity": "By", + "BusinessAddress": "Forretningsadresse 9", + "BusinessPostalCode": "0000", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/910423185.json b/test/Altinn.App.Api.Tests/Data/Register/Org/910423185.json new file mode 100644 index 000000000..5e226b296 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/910423185.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "910423185", + "Name": "EAS Health Consulting Svolvær", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010000", + "FaxNumber": "92110000", + "EMailAddress": "lofoten@eashealt.no", + "InternetAddress": "http://eashealt.no", + "MailingAddress": "Feskslogveien 12", + "MailingPostalCode": "8400", + "MailingPostalCity": "Svolvær", + "BusinessAddress": "Feskslogveien 12", + "BusinessPostalCode": "8300", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/910423495.json b/test/Altinn.App.Api.Tests/Data/Register/Org/910423495.json new file mode 100644 index 000000000..ee996843c --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/910423495.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "923609016", + "Name": "EAS Health Consulting Svolvær", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010000", + "FaxNumber": "92110000", + "EMailAddress": "lofoten@eashealt.no", + "InternetAddress": "http://eashealt.no", + "MailingAddress": "Feskslogveien 12", + "MailingPostalCode": "8400", + "MailingPostalCity": "Svolvær", + "BusinessAddress": "Feskslogveien 12", + "BusinessPostalCode": "8300", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/910457292.json b/test/Altinn.App.Api.Tests/Data/Register/Org/910457292.json new file mode 100644 index 000000000..8b1622355 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/910457292.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "910457292", + "Name": "EAS Health Consulting Sortland", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010000", + "FaxNumber": "92110000", + "EMailAddress": "sortland@eashealt.no", + "InternetAddress": "http://setrabrl.no", + "MailingAddress": "Strandgata 2", + "MailingPostalCode": "8400", + "MailingPostalCity": "Sortland", + "BusinessAddress": "Strandgata 2", + "BusinessPostalCode": "8400", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/910471120.json b/test/Altinn.App.Api.Tests/Data/Register/Org/910471120.json new file mode 100644 index 000000000..2d1b061ed --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/910471120.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "910471120", + "Name": "EAS Health Consulting Stokmarknes", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010000", + "FaxNumber": "92110000", + "EMailAddress": "sortland@eashealt.no", + "InternetAddress": "http://setrabrl.no", + "MailingAddress": "Strandgata 2", + "MailingPostalCode": "8450", + "MailingPostalCity": "Sortland", + "BusinessAddress": "Strandgata 2", + "BusinessPostalCode": "8450", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/950474084.json b/test/Altinn.App.Api.Tests/Data/Register/Org/950474084.json new file mode 100644 index 000000000..14b9aa8c5 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/950474084.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "950474084", + "Name": "Oslos Vakreste Borettslag", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010000", + "FaxNumber": "92110000", + "EMailAddress": "epost@setra.no", + "InternetAddress": "http://setrabrl.no", + "MailingAddress": "Sofies Gate 2", + "MailingPostalCode": "0170", + "MailingPostalCity": "Oslo", + "BusinessAddress": "Sofies Gate 2", + "BusinessPostalCode": "0170", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500000.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500000.json new file mode 100644 index 000000000..f51d60eb0 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500000.json @@ -0,0 +1,13 @@ +{ + "partyId": "500000", + "partyTypeName": 2, + "orgNumber": "897069650", + "ssn": null, + "unitType": "AS", + "name": "DDG Fitness", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500001.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500001.json new file mode 100644 index 000000000..eccb6a29f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500001.json @@ -0,0 +1,13 @@ +{ + "partyId": "500001", + "partyTypeName": 2, + "orgNumber": "897069651", + "ssn": null, + "unitType": "BEDR", + "name": "DDG Fitness Oslo", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500002.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500002.json new file mode 100644 index 000000000..91b04a699 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500002.json @@ -0,0 +1,13 @@ +{ + "partyId": "500002", + "partyTypeName": 2, + "orgNumber": "897069652", + "ssn": null, + "unitType": "BEDR", + "name": "DDG Fitness Bergen", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500003.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500003.json new file mode 100644 index 000000000..0ba24c6b7 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500003.json @@ -0,0 +1,13 @@ +{ + "partyId": "500003", + "partyTypeName": 2, + "orgNumber": "897069653", + "ssn": null, + "unitType": "BEDR", + "name": "DDG Fitness Trondheim", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500600.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500600.json new file mode 100644 index 000000000..417a3a3df --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500600.json @@ -0,0 +1,13 @@ +{ + "partyId": "500600", + "partyTypeName": 2, + "orgNumber": "897069631", + "ssn": null, + "unitType": "AS", + "name": "EAS Health Consulting", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500700.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500700.json new file mode 100644 index 000000000..780fe3286 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500700.json @@ -0,0 +1,13 @@ +{ + "partyId": "500700", + "partyTypeName": 2, + "orgNumber": "950474084", + "ssn": null, + "unitType": "BRL", + "name": "Oslos Vakreste Borettslag", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500800.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500800.json new file mode 100644 index 000000000..ae3d85a3b --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500800.json @@ -0,0 +1,13 @@ +{ + "partyId": "500800", + "partyTypeName": 2, + "orgNumber": "910457292", + "ssn": null, + "unitType": "AS", + "name": "EAS Health Consulting Sortland", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500801.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500801.json new file mode 100644 index 000000000..093ac8c9f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500801.json @@ -0,0 +1,13 @@ +{ + "partyId": "500801", + "partyTypeName": 2, + "orgNumber": "910471120", + "ssn": null, + "unitType": "AS", + "name": "EAS Health Consulting Stokmarknes", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500802.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500802.json new file mode 100644 index 000000000..e6eb31362 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500802.json @@ -0,0 +1,13 @@ +{ + "partyId": "500802", + "partyTypeName": 2, + "orgNumber": "910423495", + "ssn": null, + "unitType": "AS", + "name": "EAS Health Consulting Svolvær", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/501337.json b/test/Altinn.App.Api.Tests/Data/Register/Party/501337.json new file mode 100644 index 000000000..e4da6b1a9 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/501337.json @@ -0,0 +1,13 @@ +{ + "partyId": "501337", + "partyTypeName": 1, + "orgNumber": null, + "ssn": "01039012345", + "unitType": null, + "name": "Sophie Salt", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/510001.json b/test/Altinn.App.Api.Tests/Data/Register/Party/510001.json new file mode 100644 index 000000000..6649f64b5 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/510001.json @@ -0,0 +1,14 @@ +{ + "partyId": "510001", + "partyTypeName": 1, + "orgNumber": null, + "ssn": "01899699552", + "unitType": null, + "name": "Pengelens Partner", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/510002.json b/test/Altinn.App.Api.Tests/Data/Register/Party/510002.json new file mode 100644 index 000000000..bf8f73ef1 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/510002.json @@ -0,0 +1,14 @@ +{ + "partyId": "510002", + "partyTypeName": 1, + "orgNumber": null, + "ssn": "17858296439", + "unitType": null, + "name": "Gjentagende Forelder", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/510003.json b/test/Altinn.App.Api.Tests/Data/Register/Party/510003.json new file mode 100644 index 000000000..20096f3ed --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/510003.json @@ -0,0 +1,14 @@ +{ + "partyId": "510003", + "partyTypeName": 1, + "orgNumber": null, + "ssn": "08829698278", + "unitType": null, + "name": "Rik Forelder", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/512345.json b/test/Altinn.App.Api.Tests/Data/Register/Party/512345.json new file mode 100644 index 000000000..3df3c4ae8 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/512345.json @@ -0,0 +1,13 @@ +{ + "partyId": "512345", + "partyTypeName": 1, + "orgNumber": null, + "ssn": "01017512345", + "unitType": null, + "name": "Ola Nordmann", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Person/01017512345.json b/test/Altinn.App.Api.Tests/Data/Register/Person/01017512345.json new file mode 100644 index 000000000..340821484 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Person/01017512345.json @@ -0,0 +1,19 @@ +{ + "SSN": "01017512345", + "Name": "Ola Nordmann", + "FirstName": "Ola", + "MiddleName": "", + "LastName": "Nordmann", + "TelephoneNumber": "12345678", + "MobileNumber": "87654321", + "MailingAddress": "BlÃ¥bæreveien 7", + "MailingPostalCode": "8450", + "MailingPostalCity": "Stokmarknes", + "AddressMunicipalNumber": "1866", + "AddressMunicipalName": "Hadsel", + "AddressStreetName": "BlÃ¥bærveien", + "AddressHouseNumber": "7", + "AddressHouseLetter": null, + "AddressPostalCode": "8450", + "AddressCity": "Stokarknes" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Person/01039012345.json b/test/Altinn.App.Api.Tests/Data/Register/Person/01039012345.json new file mode 100644 index 000000000..038533a6b --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Person/01039012345.json @@ -0,0 +1,19 @@ +{ + "SSN": "01039012345", + "Name": "Sophie Salt", + "FirstName": "Sophie", + "MiddleName": "", + "LastName": "Salt", + "TelephoneNumber": "12345678", + "MobileNumber": "87654321", + "MailingAddress": "Grev Wedels Plass 9", + "MailingPostalCode": "0157", + "MailingPostalCity": "Oslo", + "AddressMunicipalNumber": "0301", + "AddressMunicipalName": "Oslo", + "AddressStreetName": "Grev Wedels Plass", + "AddressHouseNumber": "9", + "AddressHouseLetter": null, + "AddressPostalCode": "0151", + "AddressCity": "Oslo" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Person/01899699552.json b/test/Altinn.App.Api.Tests/Data/Register/Person/01899699552.json new file mode 100644 index 000000000..08755c1fd --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Person/01899699552.json @@ -0,0 +1,20 @@ +{ + "SSN": "01899699552", + "Name": "Pengelens Partner", + "FirstName": "Pengelens", + "MiddleName": "", + "LastName": "Partner", + "TelephoneNumber": "12345678", + "MobileNumber": "87654321", + "MailingAddress": "TuftekÃ¥svegen 7", + "MailingPostalCode": "3920", + "MailingPostalCity": "PORSGRUNN", + "AddressMunicipalNumber": "3806", + "AddressMunicipalName": "Porsgrunn", + "AddressStreetName": "TuftekÃ¥svegen", + "AddressHouseNumber": "7", + "AddressHouseLetter": null, + "AddressPostalCode": "3920", + "AddressCity": "PORSGRUNN" + } + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Register/Person/08829698278.json b/test/Altinn.App.Api.Tests/Data/Register/Person/08829698278.json new file mode 100644 index 000000000..ca0ada01d --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Person/08829698278.json @@ -0,0 +1,20 @@ +{ + "SSN": "08829698278", + "Name": "Rik Forelder", + "FirstName": "Rik", + "MiddleName": "", + "LastName": "Forelder", + "TelephoneNumber": "12345678", + "MobileNumber": "87654321", + "MailingAddress": "Bjønnkamvegen 14", + "MailingPostalCode": "3735", + "MailingPostalCity": "SKIEN", + "AddressMunicipalNumber": "3807", + "AddressMunicipalName": "Skien", + "AddressStreetName": "Bjønnkamvegen", + "AddressHouseNumber": "14", + "AddressHouseLetter": null, + "AddressPostalCode": "3735", + "AddressCity": "SKIEN" + } + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Register/Person/17858296439.json b/test/Altinn.App.Api.Tests/Data/Register/Person/17858296439.json new file mode 100644 index 000000000..38464d2ad --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Person/17858296439.json @@ -0,0 +1,20 @@ +{ + "SSN": "17858296439", + "Name": "Gjentagende Forelder", + "FirstName": "Gjentagende", + "MiddleName": "", + "LastName": "Forelder", + "TelephoneNumber": "12345678", + "MobileNumber": "87654321", + "MailingAddress": "Ørneveien 36", + "MailingPostalCode": "1640", + "MailingPostalCity": "RÃ…DE", + "AddressMunicipalNumber": "3017", + "AddressMunicipalName": "RÃ¥de", + "AddressStreetName": "Ørneveien", + "AddressHouseNumber": "36", + "AddressHouseLetter": null, + "AddressPostalCode": "1640", + "AddressCity": "RÃ…DE" + } + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/TestData.cs b/test/Altinn.App.Api.Tests/Data/TestData.cs index e67c0cd33..1f74ef50c 100644 --- a/test/Altinn.App.Api.Tests/Data/TestData.cs +++ b/test/Altinn.App.Api.Tests/Data/TestData.cs @@ -64,7 +64,7 @@ public static string GetDataBlobPath(string org, string app, int instanceOwnerId public static string GetTestDataRolesFolder(int userId, int resourcePartyId) { string testDataDirectory = GetTestDataRootDirectory(); - return Path.Combine(testDataDirectory, @"authorization/Roles/User_" + userId, "party_" + resourcePartyId, "roles.json"); + return Path.Combine(testDataDirectory, "authorization","roles", "User_" + userId, "party_" + resourcePartyId, "roles.json"); } public static string GetAltinnAppsPolicyPath(string org, string app) @@ -73,6 +73,18 @@ public static string GetAltinnAppsPolicyPath(string org, string app) return Path.Combine(testDataDirectory, "apps", org, app, "config", "authorization") + Path.DirectorySeparatorChar; } + public static string GetAltinnProfilePath() + { + string testDataDirectory = GetTestDataRootDirectory(); + return Path.Combine(testDataDirectory, "Register", "Party"); + } + + public static string GetRegisterProfilePath() + { + string testDataDirectory = GetTestDataRootDirectory(); + return Path.Combine(testDataDirectory, "Profile", "User"); + } + public static void DeleteInstance(string org, string app, int instanceOwnerId, Guid instanceGuid) { string instancePath = GetInstancePath(org, app, instanceOwnerId, instanceGuid); @@ -97,7 +109,7 @@ public static void PrepareInstance(string org, string app, int instanceOwnerId, File.Copy(preInstancePath, instancePath, true); string dataPath = GetDataDirectory(org, app, instanceOwnerId, instanceGuid); - + if (Directory.Exists(dataPath)) { foreach (string filePath in Directory.GetFiles(dataPath, "*.*", SearchOption.AllDirectories)) diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/appsettings.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/appsettings.json index 03bd1e942..3a241fba2 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/appsettings.json +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/appsettings.json @@ -7,7 +7,9 @@ } }, "AppSettings": { - "RuntimeCookieName": "AltinnStudioRuntime" + "RuntimeCookieName": "AltinnStudioRuntime", + "RequiredValidation": true, + "ExpressionValidation": true }, "GeneralSettings": { "HostName": "altinn3.no", diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/applicationmetadata.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/applicationmetadata.json index a43585704..57f86392d 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/applicationmetadata.json +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/applicationmetadata.json @@ -16,7 +16,7 @@ "maxCount": 1, "appLogic": { "autoCreate": true, - "ClassRef": "App.IntegrationTests.Mocks.Apps.tdd.custom_validation.Skjema" + "ClassRef": "Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models.Skjema" }, "taskId": "Task_1" }, diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/process/process.bpmn b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/process/process.bpmn index f28219543..2274ca316 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/process/process.bpmn +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/process/process.bpmn @@ -1,7 +1,7 @@ SequenceFlow_1n56yn5 - + SequenceFlow_1n56yn5 SequenceFlow_1oot28q + + + data + + SequenceFlow_1oot28q diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.cs b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.cs new file mode 100644 index 000000000..8ad9fe390 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.cs @@ -0,0 +1,106 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using System.Xml.Serialization; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Newtonsoft.Json; + +namespace Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models; + +public class Skjema +{ + [XmlElement("melding", Order = 1)] + [JsonProperty("melding")] + [JsonPropertyName("melding")] + public Dummy Melding { get; set; } = default!; +} + +public class Dummy +{ + [XmlElement("name", Order = 1)] + [JsonProperty("name")] + [JsonPropertyName("name")] + public string Name { get; set; } = default!; + + [XmlElement("random", Order = 2)] + [JsonProperty("random")] + [JsonPropertyName("random")] + public string Random { get; set; } = default!; + + [XmlElement("tags", Order = 3)] + [JsonProperty("tags")] + [JsonPropertyName("tags")] + public string Tags { get; set; } = default!; + + [XmlElement("simple_list", Order = 4)] + [JsonProperty("simple_list")] + [JsonPropertyName("simple_list")] + public ValuesList SimpleList { get; set; } = default!; + + [XmlElement("nested_list", Order = 5)] + [JsonProperty("nested_list")] + [JsonPropertyName("nested_list")] + public List NestedList { get; set; } = default!; + + [XmlElement("toggle", Order = 6)] + [JsonProperty("toggle")] + [JsonPropertyName("toggle")] + public bool Toggle { get; set; } = default!; + + [XmlElement("tag-with-attribute", IsNullable = true, Order = 7)] + [JsonProperty("tag-with-attribute")] + [JsonPropertyName("tag-with-attribute")] + public TagWithAttribute TagWithAttribute { get; set; } = default!; +} + +public class TagWithAttribute +{ + [Range(1, Int32.MaxValue)] + [XmlAttribute("orid")] + [BindNever] + public decimal orid { get; set; } = 34730; + + [MinLength(1)] + [MaxLength(60)] + [XmlText()] + public string? value { get; set; } +} + +public class ValuesList +{ + [XmlElement("simple_keyvalues", Order = 1)] + [JsonProperty("simple_keyvalues")] + [JsonPropertyName("simple_keyvalues")] + public List SimpleKeyvalues { get; set; } = default!; +} + +public class SimpleKeyvalues +{ + [XmlElement("key", Order = 1)] + [JsonProperty("key")] + [JsonPropertyName("key")] + public string Key { get; set; } = default!; + + [XmlElement("doubleValue", Order = 2)] + [JsonProperty("doubleValue")] + [JsonPropertyName("doubleValue")] + public decimal DoubleValue { get; set; } = default!; + + [Range(int.MinValue, int.MaxValue)] + [XmlElement("intValue", Order = 3)] + [JsonProperty("intValue")] + [JsonPropertyName("intValue")] + public decimal IntValue { get; set; } = default!; +} + +public class Nested +{ + [XmlElement("key", Order = 1)] + [JsonProperty("key")] + [JsonPropertyName("key")] + public string Key { get; set; } = default!; + + [XmlElement("values", Order = 2)] + [JsonProperty("values")] + [JsonPropertyName("values")] + public List Values { get; set; } = default!; +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/ui/Settings.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/ui/Settings.json new file mode 100644 index 000000000..7e63e1852 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/ui/Settings.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://altinncdn.no/schemas/json/layout/layoutSettings.schema.v1.json", + "pages": { + "order": [ + "page" + ] + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/ui/layouts/page.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/ui/layouts/page.json new file mode 100644 index 000000000..ca66ac17f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/ui/layouts/page.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "Heading2-2d1ba9d7-e284-4acd-a37b-e4c8ce45142a", + "type": "Header", + "size": "h2", + "textResourceBindings": { + "title": "Brukeropp-side-overskrift" + } + }, + { + "id": "name", + "type": "Input", + "required": true, + "dataModelBindings": { + "simpleBinding": "melding.name" + } + } + ] + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/appsettings.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/appsettings.json new file mode 100644 index 000000000..b1440bc4f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/appsettings.json @@ -0,0 +1,37 @@ +{ + "Kestrel": { + "EndPoints": { + "Http": { + "Url": "http://*:5005" + } + } + }, + "AppSettings": { + "OpenIdWellKnownEndpoint": "http://localhost:5101/authentication/api/v1/openid/", + "Hostname": "altinn3local.no", + "RuntimeCookieName": "AltinnStudioRuntime", + "RegisterEventsWithEventsComponent": false, + "EnableEFormidling": true + }, + "GeneralSettings": { + "HostName": "altinn3local.no", + "SoftValidationPrefix": "*WARNING*", + "AltinnPartyCookieName": "AltinnPartyId" + }, + "EFormidlingClientSettings": { + "BaseUrl": "http://localhost:9093/api/" + }, + "PlatformSettings": { + "ApiStorageEndpoint": "http://localhost:5101/storage/api/v1/", + "ApiRegisterEndpoint": "http://localhost:5101/register/api/v1/", + "ApiProfileEndpoint": "http://localhost:5101/profile/api/v1/", + "ApiAuthenticationEndpoint": "http://localhost:5101/authentication/api/v1/", + "ApiAuthorizationEndpoint": "http://localhost:5101/authorization/api/v1/", + "ApiEventsEndpoint": "http://localhost:5101/events/api/v1/", + "ApiPdfEndpoint": "http://localhost:5070/api/v1/", + "SubscriptionKey": "retrieved from environment at runtime" + }, + "ApplicationInsights": { + "InstrumentationKey": "retrieved from environment at runtime" + } +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/applicationmetadata.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/applicationmetadata.json new file mode 100644 index 000000000..04fdae29e --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/applicationmetadata.json @@ -0,0 +1,53 @@ +{ + "id": "ttd/task-action", + "org": "ttd", + "title": { + "nb": "task-action" + }, + "dataTypes": [ + { + "id": "ref-data-as-pdf", + "allowedContentTypes": [ + "application/pdf" + ], + "maxCount": 0, + "minCount": 0, + "enablePdfCreation": true, + "enableFileScan": false, + "validationErrorOnPendingFileScan": false, + "enabledFileAnalysers": [], + "enabledFileValidators": [] + }, + { + "id": "Scheme", + "allowedContentTypes": [ + "application/xml" + ], + "appLogic": { + "autoCreate": true, + "classRef": "Altinn.App.Models.Scheme", + "allowAnonymousOnStateless": false, + "autoDeleteOnProcessEnd": false + }, + "taskId": "Task_1", + "maxCount": 1, + "minCount": 1, + "enablePdfCreation": true, + "enableFileScan": false, + "validationErrorOnPendingFileScan": false, + "enabledFileAnalysers": [], + "enabledFileValidators": [] + } + ], + "partyTypesAllowed": { + "bankruptcyEstate": false, + "organisation": false, + "person": false, + "subUnit": false + }, + "autoDeleteOnProcessEnd": false, + "created": "2023-05-31T08:03:25.9385888Z", + "createdBy": "tjololo", + "lastChanged": "2023-05-31T08:03:25.9385925Z", + "lastChangedBy": "tjololo" +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/authorization/policy.xml b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/authorization/policy.xml new file mode 100644 index 000000000..3e3697176 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/authorization/policy.xml @@ -0,0 +1,401 @@ + + + + + A rule giving user with role REGNA or DAGL and the app owner tdd the right to instantiate a + instance of a given app of tdd/task-actions + + + + + + REGNA + + + + + + DAGL + + + + + + tdd + + + + + + + + tdd + + + + task-actions + + + + + + + + instantiate + + + + + + read + + + + + + + + Rule that defines that user with role REGNA or DAGL can read, write, lookup, toconfirm and complete for + tdd/task-actions when it is in Task_1 + + + + + + REGNA + + + + + + DAGL + + + + + + + + tdd + + + + task-actions + + + + Task_1 + + + + + + + + read + + + + + + write + + + + + + complete + + + + + + lookup + + + + + + + + Rule that defines that user with role REGNA or DAGL can delete instances of tdd/task-actions + + + + + + REGNA + + + + + + DAGL + + + + + + + + tdd + + + + task-actions + + + + delete + + + + + + + + Rule that defines that org can write to instances of tdd/task-actions for any states + + + + + + tdd + + + + + + + + tdd + + + + task-actions + + + + + + + + write + + + + + + + + Rule that defines that org can complete an instance of tdd/task-actions which state is at the end + event. + + + + + + tdd + + + + + + + + tdd + + + + task-actions + + + + EndEvent_1 + + + + + + + + complete + + + + + + + + A rule giving user with role REGNA or DAGL and the app owner tdd the right to read the + appresource events of a given app of tdd/task-actions + + + + + + REGNA + + + + + + DAGL + + + + + + tdd + + + + + + + + tdd + + + + task-actions + + + + events + + + + + + + + read + + + + + + + + + + 2 + + + + diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/process/process.bpmn b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/process/process.bpmn new file mode 100644 index 000000000..97306f98c --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/process/process.bpmn @@ -0,0 +1,26 @@ + + + + + SequenceFlow_1 + + + SequenceFlow_1 + SequenceFlow_2 + + + data + + complete + lookup + + + + + + SequenceFlow_2 + + + + + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/texts/resource.nb.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/texts/resource.nb.json new file mode 100644 index 000000000..0a628564e --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/texts/resource.nb.json @@ -0,0 +1,33 @@ +{ + "language": "nb", + "resources": [ + { + "id": "appName", + "value": "vga-simple-app" + }, + { + "id": "Side1.Input-P4VNaW.title", + "value": "Navn" + }, + { + "id": "Side1.Input-P4VNaX.title", + "value": "Regex validation" + }, + { + "id": "Side1.Button-Lj0cYD.title", + "value": "Til signering" + }, + { + "id": "Side1.Button-Lj0cYE.title", + "value": "Fullfør" + }, + { + "id": "Side1.Button-Lj0cYF.title", + "value": "Bekreft" + }, + { + "id": "Side1.Button-Lj0cYG.title", + "value": "Avvis" + } + ] +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/claims/12345.json b/test/Altinn.App.Api.Tests/Data/authorization/claims/12345.json new file mode 100644 index 000000000..e3da13258 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/claims/12345.json @@ -0,0 +1,7 @@ +[ + { + "type": "some:extra:claim", + "value": "claimValue", + "valueType": "http://www.w3.org/2001/XMLSchema#string" + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/claims/1337.json b/test/Altinn.App.Api.Tests/Data/authorization/claims/1337.json new file mode 100644 index 000000000..e3da13258 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/claims/1337.json @@ -0,0 +1,7 @@ +[ + { + "type": "some:extra:claim", + "value": "claimValue", + "valueType": "http://www.w3.org/2001/XMLSchema#string" + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/partylist/1001.json b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1001.json new file mode 100644 index 000000000..d48be7f3c --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1001.json @@ -0,0 +1,25 @@ +[ + { + "partyId": "510001", + "partyTypeName": 1, + "ssn": "01899699552", + "name": "Pengelens Partner", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500000", + "partyTypeName": 2, + "OrgNumber": "897069650", + "unitType": "AS", + "name": "DDG Fitness AS", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } + ] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/partylist/1002.json b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1002.json new file mode 100644 index 000000000..928dce83a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1002.json @@ -0,0 +1,25 @@ +[ + { + "partyId": "510002", + "partyTypeName": 1, + "ssn": "17858296439", + "name": "Gjentagende Forelder", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500000", + "partyTypeName": 2, + "OrgNumber": "897069650", + "unitType": "AS", + "name": "DDG Fitness AS", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } + ] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/partylist/1003.json b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1003.json new file mode 100644 index 000000000..d5ebed7e5 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1003.json @@ -0,0 +1,13 @@ +[ + { + "partyId": "510003", + "partyTypeName": 1, + "ssn": "08829698278", + "name": "Rik Forelder", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } + ] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/partylist/12345.json b/test/Altinn.App.Api.Tests/Data/authorization/partylist/12345.json new file mode 100644 index 000000000..da7122420 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/partylist/12345.json @@ -0,0 +1,13 @@ +[ + { + "partyId": "512345", + "partyTypeName": 1, + "ssn": "01017512345", + "name": "Ola Nordmann", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/partylist/1337.json b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1337.json new file mode 100644 index 000000000..eef98968c --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1337.json @@ -0,0 +1,124 @@ +[ + { + "partyId": "501337", + "partyTypeName": 1, + "ssn": "01038812345", + "unitType": null, + "name": "Sophie Salt", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500000", + "partyTypeName": 2, + "OrgNumber": "897069650", + "unitType": "AS", + "name": "DDG Fitness AS", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": [ + { + "partyId": "500001", + "partyTypeName": 2, + "OrgNumber": "897069651", + "unitType": "BEDR", + "name": "DDG Fitness Bergen", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500002", + "partyTypeName": 2, + "OrgNumber": "897069652", + "unitType": "BEDR", + "name": "DDG Fitness Oslo", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500003", + "partyTypeName": 2, + "OrgNumber": "897069653", + "unitType": "BEDR", + "name": "DDG Fitness Trondheim", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } + + ] + }, + { + "partyId": "500600", + "partyTypeName": 2, + "OrgNumber": "897069631", + "unitType": "AS", + "name": "EAS Health Consulting", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500700", + "partyTypeName": 2, + "OrgNumber": "950474084", + "unitType": "BRL", + "name": "Oslos Vakreste Borettslag", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500800", + "partyTypeName": 2, + "OrgNumber": "910457292", + "unitType": "AS", + "name": "EAS Health Consulting Sortland", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500801", + "partyTypeName": 2, + "OrgNumber": "910471120", + "unitType": "AS", + "name": "EAS Health Consulting Stokmarknes", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500802", + "partyTypeName": 2, + "OrgNumber": "910423495", + "unitType": "AS", + "name": "EAS Health Consulting Svolvær", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_119.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_119.json new file mode 100644 index 000000000..1d85c7d2b --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_119.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-04T18:04:27.27", + "identifier": "appid-119", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "8f08210a-d792-48f5-9e27-0f029e41111e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:119", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_120.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_120.json new file mode 100644 index 000000000..80074640d --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_120.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-04T19:07:46.143", + "identifier": "appid-120", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "a6574ca8-5836-46b0-91f0-8ebb0ff214cf", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:120", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_122.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_122.json new file mode 100644 index 000000000..3a3dd8529 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_122.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-06T09:36:15.443", + "identifier": "appid-122", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "da7650a5-d893-404a-9bfe-28d0904fe896", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:122", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_123.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_123.json new file mode 100644 index 000000000..350ae8793 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_123.json @@ -0,0 +1,65 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-06T09:41:15.817", + "identifier": "appid-123", + "isComplete": false, + "description": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "3cb27d7b-9e2c-475c-ba91-e1fc359bc717", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:123", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076", + "name": { + "en": "Accenture Norway", + "nb-no": "Accenture Norge", + "nn-no": "Accenture Norge" + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_124.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_124.json new file mode 100644 index 000000000..71e9ffb58 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_124.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-25T15:17:18.487", + "identifier": "appid-124", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "fb1bc117-333d-4dea-9400-6bd3e1ce07dd", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:124", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_125.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_125.json new file mode 100644 index 000000000..6098f6cf5 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_125.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-25T15:20:40.17", + "identifier": "appid-125", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "c8e7b227-3d62-4682-81a2-f2865f0eb91f", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:125", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_126.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_126.json new file mode 100644 index 000000000..0710b4e31 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_126.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-13T11:10:52.203", + "identifier": "appid-126", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "4f9b0f81-74a6-4b76-8ce5-0e1fe153c872", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:126", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_127.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_127.json new file mode 100644 index 000000000..642932107 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_127.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-19T11:13:36.8", + "identifier": "appid-127", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "38da61a2-4cd8-4dac-9d6b-8205f91661c4", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:127", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_128.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_128.json new file mode 100644 index 000000000..e45c37f2a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_128.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-19T11:18:52.84", + "identifier": "appid-128", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "74f5dcb3-eee0-4a3b-9499-93fc2148a513", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:128", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_129.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_129.json new file mode 100644 index 000000000..0d3aeeb48 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_129.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-20T11:49:19.26", + "identifier": "appid-129", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "d0fc231c-aaa3-4e6a-bece-4e283e6e2f6f", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:129", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_130.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_130.json new file mode 100644 index 000000000..14de58b9f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_130.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-20T12:32:41.78", + "identifier": "appid-130", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "3fc8a856-d5b1-4861-ace0-6b3fd5ffd916", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:130", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_132.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_132.json new file mode 100644 index 000000000..b3a050708 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_132.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-28T23:38:06.337", + "identifier": "appid-132", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "923bdaaf-0a5e-4b63-908b-4a5ecf75e37b", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:132", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_133.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_133.json new file mode 100644 index 000000000..3eaa938d8 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_133.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-28T23:39:09.01", + "identifier": "appid-133", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "f7670967-0428-4b90-86a2-2ac637d0ec2a", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:133", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_134.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_134.json new file mode 100644 index 000000000..0b1de5d2f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_134.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-28T23:40:31.413", + "identifier": "appid-134", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "d812fd77-7a43-4332-a782-375b37aff1eb", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:134", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_136.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_136.json new file mode 100644 index 000000000..c0c70931d --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_136.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-29T09:50:19.333", + "identifier": "appid-136", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "911f9e93-1541-429d-a8c6-a06f9a52d827", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:136", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "MAT", + "organization": "985399077" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_137.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_137.json new file mode 100644 index 000000000..d1d86b5b9 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_137.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-06-05T14:05:14.83", + "identifier": "appid-137", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "938a1d17-5015-4a17-bd20-575bf5cd1102", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:137", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_138.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_138.json new file mode 100644 index 000000000..6596b80ca --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_138.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-06-17T12:46:18.437", + "identifier": "appid-138", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "b4c2ac55-fa7d-44d1-8c95-e64d47206a57", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:138", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SLK", + "organization": "960885406" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_139.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_139.json new file mode 100644 index 000000000..f2dabdc54 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_139.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Test-API for a demo", + "nb": "Test-API for demo", + "nn": "Test-API for ein demo" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-08-27T08:15:12.673", + "identifier": "appid-139", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "This service grants full access to a test API", + "nb": "Denne tjenesten gir full tilgang til et test-API", + "nn": "Denne tenesta gir full tilgang til eit test-API" + }, + "resourceReferences": [ + { + "reference": "0f184d85-afa3-4dcf-916b-5a8f85a12c95", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:ettellerannetscope.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:ettellerannetscope.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:139", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_142.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_142.json new file mode 100644 index 000000000..333d8aada --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_142.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-09-11T07:32:49.723", + "identifier": "appid-142", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "1f6952b3-84ef-4eec-b742-846d986691e3", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:142", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_144.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_144.json new file mode 100644 index 000000000..cd78ea3f7 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_144.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "JK's Security Level 3 Scheme", + "nb": "JK's SikkerhetsnivÃ¥ 3 Scheme", + "nn": "JK's SikkringsnivÃ¥ 3 Scheme" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-09-22T11:46:26.11", + "identifier": "appid-144", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Example of a DelegationScheme with security level 3 requirement", + "nb": "Eksempel pÃ¥ DelegationScheme med sikkerhetsnivÃ¥ 3 krav", + "nn": "Eit døme pÃ¥ DelegationScheme med sikkringsnivÃ¥ 3 krav" + }, + "resourceReferences": [ + { + "reference": "5f03a306-8a01-47b5-8219-8014843ce691", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/seclvl3.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/seclvl3.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:144", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_145.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_145.json new file mode 100644 index 000000000..c8b1b9329 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_145.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "JK's Security Level 2 Scheme", + "nb": "JK's SikkerhetsnivÃ¥ 2 Scheme", + "nn": "JK's SikkringsnivÃ¥ 2 Scheme" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-09-22T11:54:03.32", + "identifier": "appid-145", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Example of a DelegationScheme with security level 2 requirement", + "nb": "Eksempel pÃ¥ DelegationScheme med sikkerhetsnivÃ¥ 2 krav", + "nn": "Eit døme pÃ¥ DelegationScheme med sikkringsnivÃ¥ 2 krav" + }, + "resourceReferences": [ + { + "reference": "6685c771-459a-43ac-acb0-27a46d8f32d1", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/seclvl2.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/seclvl2.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:145", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_147.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_147.json new file mode 100644 index 000000000..487c6f9c2 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_147.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-09-29T12:32:40.68", + "identifier": "appid-147", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "08d6bac5-e581-4e34-b33e-1f3e2994159a", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:147", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_148.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_148.json new file mode 100644 index 000000000..549efffd1 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_148.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-09-30T10:57:40.96", + "identifier": "appid-148", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "6d91d828-5554-48f9-8875-3a5cb1cf562e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:148", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_150.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_150.json new file mode 100644 index 000000000..ce49e7beb --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_150.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-11-23T08:36:19.6", + "identifier": "appid-150", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "77b6ba03-b0e0-4b38-8477-0928f4a2131c", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:150", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_153.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_153.json new file mode 100644 index 000000000..20455dd90 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_153.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-12-16T11:19:44.693", + "identifier": "appid-153", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "4ee353be-b998-4e86-8366-99a9c8ece1bc", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:153", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_154.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_154.json new file mode 100644 index 000000000..b16078058 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_154.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-01-10T22:55:28.887", + "identifier": "appid-154", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "630031e8-8074-4e85-b39d-b5ec523354fa", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:154", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "NAV", + "organization": "889640782" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_155.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_155.json new file mode 100644 index 000000000..482e609eb --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_155.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-01-12T20:47:54.047", + "identifier": "appid-155", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "7a44f919-f4a6-4ac1-8caf-0d364e8f355f", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:155", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_164.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_164.json new file mode 100644 index 000000000..ad4eb5554 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_164.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-01-15T09:06:22.017", + "identifier": "appid-164", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "5fec884c-c232-456f-8b2f-9e54c0d1efb3", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:164", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_168.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_168.json new file mode 100644 index 000000000..09088b84e --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_168.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-01-28T18:05:06.683", + "identifier": "appid-168", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "30168308-9f93-460f-868a-0edbb921ea8f", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:168", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_178.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_178.json new file mode 100644 index 000000000..9cafa317d --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_178.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-02-15T19:51:35.823", + "identifier": "appid-178", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "04e68c88-92f4-42a1-a464-a97f41d7a58f", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:178", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_179.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_179.json new file mode 100644 index 000000000..49dd3cf11 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_179.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-02-15T19:54:20.687", + "identifier": "appid-179", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "692cc835-60b0-49d7-b10d-2f9fa3c07eec", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:179", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_180.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_180.json new file mode 100644 index 000000000..5915865b3 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_180.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-02-15T19:57:22.347", + "identifier": "appid-180", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "da9b46e2-479f-44c1-8423-01c3a0fab56c", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:180", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_181.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_181.json new file mode 100644 index 000000000..49c8b0224 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_181.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-02-15T19:57:28.673", + "identifier": "appid-181", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "73085841-e264-4257-bc0c-061c5707d54b", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:181", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_182.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_182.json new file mode 100644 index 000000000..6e2a47cca --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_182.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Regression", + "nb": "Regression", + "nn": "Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-02-15T19:57:42.953", + "identifier": "appid-182", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes", + "nb": "Gir anledning til Ã¥ teste maskinporten", + "nn": "Gjer høve til Ã¥ teste maskinporten" + }, + "resourceReferences": [ + { + "reference": "c6086d53-6d60-41b3-9f67-32a6347222e2", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:182", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_184.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_184.json new file mode 100644 index 000000000..c1e0a94e9 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_184.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-02-16T16:01:27.67", + "identifier": "appid-184", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "3210f4eb-2d76-4666-9574-664c0fd985f5", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:184", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_185.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_185.json new file mode 100644 index 000000000..a8139aa6a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_185.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-02-16T16:02:03.783", + "identifier": "appid-185", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "165a2af4-a9e3-4d94-a5f2-65ec749af10e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:185", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_191.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_191.json new file mode 100644 index 000000000..2adff8082 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_191.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-09T14:11:22.853", + "identifier": "appid-191", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "86a49da7-9d41-42ba-9ab8-0ded1f6b6b34", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:191", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SVV", + "organization": "971032081" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_192.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_192.json new file mode 100644 index 000000000..4918f9cb6 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_192.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-09T14:11:34.503", + "identifier": "appid-192", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "cc8878d7-21d2-43af-ae4a-27ffcd0f06e4", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:192", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SVV", + "organization": "971032081" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_193.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_193.json new file mode 100644 index 000000000..057ff5b11 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_193.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-09T20:12:53.51", + "identifier": "appid-193", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "47a69357-3028-48bc-abdf-1fd4857446bb", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:193", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_196.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_196.json new file mode 100644 index 000000000..b79f49601 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_196.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T13:59:35.29", + "identifier": "appid-196", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "b6bdc87c-e124-437b-9858-35f27171a24a", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:196", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_197.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_197.json new file mode 100644 index 000000000..dc409fbe5 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_197.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T13:59:39.577", + "identifier": "appid-197", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "09ef4c47-0493-49ba-ba69-7028df31134c", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:197", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_198.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_198.json new file mode 100644 index 000000000..ee625ae16 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_198.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T13:59:43.623", + "identifier": "appid-198", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "98e7d274-3b64-4d10-b7d4-2b6e27026350", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:198", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_199.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_199.json new file mode 100644 index 000000000..a154221dc --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_199.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T13:59:47.017", + "identifier": "appid-199", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "6ac1efeb-0ae7-4e3d-aafd-5bfe2184e348", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:199", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_200.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_200.json new file mode 100644 index 000000000..49d3a9717 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_200.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T13:59:49.973", + "identifier": "appid-200", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "0d552f1c-dc41-43a6-9f76-4650434ed5e1", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:200", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_201.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_201.json new file mode 100644 index 000000000..6e4fe8038 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_201.json @@ -0,0 +1,41 @@ +{ + "title": { + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T13:59:54.017", + "identifier": "appid-201", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "45164e1d-147b-4c51-8446-19d9233a0c27", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:201", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_202.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_202.json new file mode 100644 index 000000000..a95c85c38 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_202.json @@ -0,0 +1,38 @@ +{ + "title": {}, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T13:59:57.35", + "identifier": "appid-202", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "d60d98dd-830e-4554-b2d4-b9b76696f770", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:202", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_203.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_203.json new file mode 100644 index 000000000..596445af3 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_203.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T14:00:00.53", + "identifier": "appid-203", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "eb7a322c-98f5-4d91-8720-baf4b7a61dcc", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:203", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_204.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_204.json new file mode 100644 index 000000000..caa1fc95a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_204.json @@ -0,0 +1,38 @@ +{ + "title": {}, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T14:00:04.45", + "identifier": "appid-204", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "18ab3b9a-a0c2-408c-b9fa-21838330c2fa", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:204", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_205.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_205.json new file mode 100644 index 000000000..89127804d --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_205.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-05-18T13:47:29.147", + "identifier": "appid-205", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "92e7f534-b126-415c-8a65-d7a8d8ff70a5", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:205", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SVV", + "organization": "971032081" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_206.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_206.json new file mode 100644 index 000000000..fd5fec1e9 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_206.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-08-10T09:46:01.027", + "identifier": "appid-206", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "72f5b1bd-3e0e-49d0-8133-236cb8b628e5", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:206", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_207.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_207.json new file mode 100644 index 000000000..a6a265f1c --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_207.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-08-10T09:47:12.093", + "identifier": "appid-207", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "71cee800-9b2c-4326-b336-7921223f9ca3", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:207", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_208.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_208.json new file mode 100644 index 000000000..368e9e320 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_208.json @@ -0,0 +1,54 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-08-10T09:47:54.12", + "identifier": "appid-208", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "fd9a3748-2c56-49dd-9860-64de0258ada3", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:208", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_209.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_209.json new file mode 100644 index 000000000..303ab013b --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_209.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-08-10T09:48:49.723", + "identifier": "appid-209", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "f011cd46-d7f3-4037-975c-5bdcd166295e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:209", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_210.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_210.json new file mode 100644 index 000000000..3969827d9 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_210.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-08-10T10:01:35.137", + "identifier": "appid-210", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "55d73b7c-6040-41ff-b0dc-877692ba0ea6", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:210", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_211.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_211.json new file mode 100644 index 000000000..3905088b1 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_211.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-08-12T13:27:33.327", + "identifier": "appid-211", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "43a73290-3193-49be-89f5-2b309e373442", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:211", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_212.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_212.json new file mode 100644 index 000000000..df15544ee --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_212.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-08-18T14:22:38", + "identifier": "appid-212", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "5295d5bd-c4c4-43c4-a55e-7c15ecde1ce7", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:212", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_213.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_213.json new file mode 100644 index 000000000..2179c75b6 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_213.json @@ -0,0 +1,52 @@ +{ + "title": { + "nb": "Bjørn 1" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-09-03T12:30:33.887", + "identifier": "appid-213", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "nb": "Bla bla bla bla bla" + }, + "resourceReferences": [ + { + "reference": "8931d72d-0bab-4c9f-a260-ce37810d6f16", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:213", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_214.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_214.json new file mode 100644 index 000000000..56a4a6261 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_214.json @@ -0,0 +1,52 @@ +{ + "title": { + "nb": "Bjørn 1" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-09-28T13:55:27.403", + "identifier": "appid-214", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "nb": "Bla bla bla bla bla" + }, + "resourceReferences": [ + { + "reference": "61aeb251-1e1c-4dcd-b33a-6e63e30b0e6f", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:214", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_215.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_215.json new file mode 100644 index 000000000..6f1eb9e5f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_215.json @@ -0,0 +1,52 @@ +{ + "title": { + "nb": "Automation Test Delegation Scheme Requires Level 3" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-10-27T16:11:59.113", + "identifier": "appid-215", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "nb": "Bla bla bla bla bla" + }, + "resourceReferences": [ + { + "reference": "89058b75-8ddb-4c69-8e5c-158b87f7e6cc", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:215", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "NAV", + "organization": "889640782" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_216.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_216.json new file mode 100644 index 000000000..70cec5881 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_216.json @@ -0,0 +1,52 @@ +{ + "title": { + "nb": "Automation Test Delegation Scheme Requires Level 3" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-10-27T16:12:00.827", + "identifier": "appid-216", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "nb": "Bla bla bla bla bla" + }, + "resourceReferences": [ + { + "reference": "9fd1e158-2dad-4e56-bf7f-ab2e69b73a0a", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:216", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "NAV", + "organization": "889640782" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_217.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_217.json new file mode 100644 index 000000000..2ca4d15b2 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_217.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-12-13T12:12:34.827", + "identifier": "appid-217", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "ea6e52be-b4d6-44d3-a272-f955b84b34e6", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:217", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_218.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_218.json new file mode 100644 index 000000000..bc5669531 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_218.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-12-13T12:12:49.563", + "identifier": "appid-218", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "ed752be2-e80b-4cc5-83e5-263dbc1cf60d", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:218", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_219.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_219.json new file mode 100644 index 000000000..fc2f147b7 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_219.json @@ -0,0 +1,38 @@ +{ + "title": {}, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2022-03-01T13:28:17.143", + "identifier": "appid-219", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "5b16379f-f999-4805-9a67-2c32ce304ffa", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:219", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DFO", + "organization": "986252932" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_220.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_220.json new file mode 100644 index 000000000..b023e8a2e --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_220.json @@ -0,0 +1,38 @@ +{ + "title": {}, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2022-04-01T15:29:03.213", + "identifier": "appid-220", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "122cea91-6b64-4ea4-b460-3ab597ffe15d", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:220", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "NHN", + "organization": "994598759" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_221.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_221.json new file mode 100644 index 000000000..aaf28ddb8 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_221.json @@ -0,0 +1,38 @@ +{ + "title": {}, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2022-04-20T13:31:33.433", + "identifier": "appid-221", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "fae69028-5497-432f-9a32-d7821b487fff", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:221", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "MAT", + "organization": "985399077" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_222.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_222.json new file mode 100644 index 000000000..4aac44a02 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_222.json @@ -0,0 +1,38 @@ +{ + "title": {}, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2022-06-08T09:45:50.453", + "identifier": "appid-222", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "95c94595-c026-4b7b-880a-7c03d9736ceb", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:222", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_223.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_223.json new file mode 100644 index 000000000..b0221008f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_223.json @@ -0,0 +1,38 @@ +{ + "title": {}, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2022-06-13T13:41:02.943", + "identifier": "appid-223", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "2d04277f-f4a9-498f-b317-8bfacb0d19e9", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:223", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_400.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_400.json new file mode 100644 index 000000000..f1b3f7921 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_400.json @@ -0,0 +1,60 @@ +{ + "title": { + "en": "Humbug Registry", + "nb": "Tulleregisteret", + "nn": "Tulleregisteret" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-04T18:04:27.27", + "identifier": "appid-400", + "isComplete": false, + "description": { + "en": "Humbug Registry", + "nb": "Tulleregisteret", + "nn": "Tulleregisteret" + }, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Gives access to silly things.", + "nb": "Gir tilgang til tullete ting.", + "nn": "Gir tilgang til tullete ting." + }, + "resourceReferences": [ + { + "reference": "8f08210a-d792-48f5-9e27-0f029e41111e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "nav:paa/v1/luring", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:400", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "PAA", + "organization": "985399077", + "name": { + "en": "DEPARTMENT OF HUMBUG", + "nb": "PÃ…FUNNSETATEN", + "nn": "PÃ…FUNNSETATEN" + } + } +} diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_401.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_401.json new file mode 100644 index 000000000..e46e160a8 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_401.json @@ -0,0 +1,65 @@ +{ + "title": { + "en": "The Flowergarden", + "nb": "Blomsterhagen", + "nn": "Blomsterhagen" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-04T18:04:27.27", + "identifier": "appid-401", + "isComplete": false, + "description": { + "en": "The Flowergarden", + "nb": "Blomsterhagen", + "nn": "Blomsterhagen" + }, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Gives access to beautiful flowers.", + "nb": "Gir tilgang til vakre blomster.", + "nn": "Gir tilgang til vakre blomster." + }, + "resourceReferences": [ + { + "reference": "8f08210a-d792-48f5-9e27-0f029e41111e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "nav:aareg/v1/arbeidsforhold/otp", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "nav:bfinn/v1/hagen", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:401", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "BFINN", + "organization": "994598759", + "name": { + "en": "BLOMSTERFINN", + "nb": "BLOMSTERFINN", + "nn": "BLOMSTERFINN" + } + } +} diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_402.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_402.json new file mode 100644 index 000000000..bb1ce84ed --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_402.json @@ -0,0 +1,65 @@ +{ + "title": { + "en": "The Magic Closet", + "nb": "Det magiske klesskapet", + "nn": "Det magiske klesskapet" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-04T18:04:27.27", + "identifier": "appid-402", + "isComplete": false, + "description": { + "en": "The Magic Closet", + "nb": "Det magiske klesskapet", + "nn": "Det magiske klesskapet" + }, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Gives access to Narnia.", + "nb": "Gir tilgang til Narnia.", + "nn": "Gir tilgang til Narnia." + }, + "resourceReferences": [ + { + "reference": "8f08210a-d792-48f5-9e27-0f029e41111e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "nav:nrna/v1/kaape/otp", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "nav:nrna/huset/dettommerommet", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:402", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "NRNA", + "organization": "971032081", + "name": { + "en": "NARNIA", + "nb": "NARNIA", + "nn": "NARNIA" + } + } +} diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_403.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_403.json new file mode 100644 index 000000000..17785d24a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_403.json @@ -0,0 +1,60 @@ +{ + "title": { + "en": "The Shortcut", + "nb": "Snarveien", + "nn": "Snarvegen" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-04T18:04:27.27", + "identifier": "appid-403", + "isComplete": false, + "description": { + "en": "The Shortcut", + "nb": "Snarveien", + "nn": "Snarvegen" + }, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Gives access to shortcuts.", + "nb": "Gir tilgang til snarveier.", + "nn": "Gir tilgang til snarveger." + }, + "resourceReferences": [ + { + "reference": "8f08210a-d792-48f5-9e27-0f029e41111e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "nav:paa/v1/snartenkt", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:400", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "PAA", + "organization": "985399077", + "name": { + "en": "DEPARTMENT OF HUMBUG", + "nb": "PÃ…FUNNSETATEN", + "nn": "PÃ…FUNNSETATEN" + } + } +} diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_43.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_43.json new file mode 100644 index 000000000..893c24894 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_43.json @@ -0,0 +1,60 @@ +{ + "title": { + "en": "Aa-registeret OTP API", + "nb": "Aa-registeret OTP API", + "nn": "Aa-registeret OTP API" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-04T18:04:27.27", + "identifier": "appid-43", + "isComplete": false, + "description": { + "en": "Aa-registeret OTP API", + "nb": "Aa-registeret OTP API", + "nn": "Aa-registeret OTP API" + }, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Gives the pension cooperation access to employee relationships registered to a legal entity.", + "nb": "Gir pensjonsinnretningen tilgang til arbeidsforhold registrert pÃ¥ en opplysningspliktig.", + "nn": "Gir pensjonsinnretninga tilgang pÃ¥ dei arbeidforholda som er registrert pÃ¥ ein opplysingspliktig." + }, + "resourceReferences": [ + { + "reference": "8f08210a-d792-48f5-9e27-0f029e41111e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "nav:aareg/v1/arbeidsforhold/otp", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:43", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "NAV", + "organization": "974761076", + "name": { + "en": "ARBEIDS- OG VELFERDSETATEN", + "nb": "ARBEIDS- OG VELFERDSETATEN", + "nn": "ARBEIDS- OG VELFERDSETATEN" + } + } +} diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/altinn_access_management.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/altinn_access_management.json new file mode 100644 index 000000000..8cefddb93 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/altinn_access_management.json @@ -0,0 +1,26 @@ +{ + "identifier": "altinn_access_management", + "description": { + "nb": "Funksjonalitet for � tilgangsstyring I Altinn" + }, + "title": { + "nb": "Tilgangsstyring Altinn" + }, + "hasCompetentAuthority": { + "organization": "991825827", + "orgcode": "digdir" + }, + "contactpoint": [ + { + "phone": "1231324", + "email": "online@digdir.no" + } + ], + "isPartOf": "altiinnportal", + "hasPart": "delegationrequests", + "homepage": "www.altinn.no", + "status": "Completed", + "thematicArea": "http://publications.europa.eu/resource/authority/eurovoc/2468", + "type": [], + "sector": [] +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/nav_tiltakAvtaleOmArbeidstrening.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/nav_tiltakAvtaleOmArbeidstrening.json new file mode 100644 index 000000000..f8c744021 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/nav_tiltakAvtaleOmArbeidstrening.json @@ -0,0 +1,46 @@ +{ + "identifier": "nav_tiltakAvtaleOmArbeidstrening", + "title": { + "nb": "Avtale om arbeidstrening", + "en": "Agreement on work training" + }, + "description": { + "nb-NO": "Avtalen skal bidra til at personer med behov for bistand fra NAV skal få eller beholde en jobb, samtidig som den støtter arbeidsgivere som inkluderer deg som har nedsatt arbeidsevne. Avtalen inngås mellom arbeidsgiver, arbeidssøker og NAV.", + "EN": "The agreement will help people who need assistance from NAV to get or keep a job, while supporting employers who include those who have reduced working capacity. The agreement is entered into between the employer, the jobseeker and NAV." + }, + "rightDescription": { + "nb-NO": "Med denne fullmakten kan man inngå en avtale om arbeidstreing med NAV og kommune.", + "EN": "With this authorisation, you can enter into an agreement on work training with NAV and the municipality" + }, + "hasCompetentAuthority": { + "organization": "889640782", + "orgcode": "NAV" + }, + "contactpoint": [ + { + "phone": "55 55 33 36 ", + "email": "postmottak@nav.no" + } + ], + "homepage": "https://www.nav.no/", + "status": "Active", + "validFrom": "2019-05-08T14:00:00", + "validTo": "2049-05-08T14:00:00", + "isPartOf": "nav_RapporteringOmArbeidstrening", + "isPublicService": true, + "usedBy": [ "urn:citizen", "urn:enterprise", "urn:selfidentifeduser" ], + "thematicArea": "http://publications.europa.eu/resource/authority/eurovoc/2468", + "type": [], + "sector": [], + "keywords": [ + { + "word" : "Dødsbo", + "language" : "nb" + } + ], + "resourceReference": { + "referenceSource": "Altinn2", + "referenceType": "ServiceCodeVersion", + "reference": "5332/2" + } +} diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/policies/altinn_maskinporten_scope_delegation.xml b/test/Altinn.App.Api.Tests/Data/authorization/resources/policies/altinn_maskinporten_scope_delegation.xml new file mode 100644 index 000000000..f86c15c26 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/policies/altinn_maskinporten_scope_delegation.xml @@ -0,0 +1,52 @@ + + + + + Eksempel pÃ¥ samleregel som spesifiserer at bÃ¥de REGNA og DAGL m/sikkerhetsnivÃ¥; 2, for ressursen; SKD/TaxReport fÃ¥r tilgang til operasjonene; Read, Write og Instantiate for Event; Tasks; FormFilling og Signing + + + + + regna + + + + + + dagl + + + + + + + + altinn_maskinporten_scope_delegation + + + + + + + + read + + + + + + write + + + + + + + + + + 2 + + + + diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1001/party_500000/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1001/party_500000/roles.json new file mode 100644 index 000000000..ee27c7b99 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1001/party_500000/roles.json @@ -0,0 +1,14 @@ +[ + { + "Type": "altinn", + "value": "A0239" + }, + { + "Type": "altinn", + "value": "A0240" + }, + { + "Type": "altinn", + "value": "A0241" + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_3/party_1003/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1001/party_510001/roles.json similarity index 66% rename from test/Altinn.App.Api.Tests/Data/authorization/roles/user_3/party_1003/roles.json rename to test/Altinn.App.Api.Tests/Data/authorization/roles/User_1001/party_510001/roles.json index c54bcd426..ebdd9bba3 100644 --- a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_3/party_1003/roles.json +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1001/party_510001/roles.json @@ -6,5 +6,9 @@ { "Type": "altinn", "value": "dagl" + }, + { + "Type": "altinn", + "value": "priv" } -] +] \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_1/party_1000/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1002/party_500000/roles.json similarity index 60% rename from test/Altinn.App.Api.Tests/Data/authorization/roles/user_1/party_1000/roles.json rename to test/Altinn.App.Api.Tests/Data/authorization/roles/User_1002/party_500000/roles.json index eb5c181bd..a416c98b3 100644 --- a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_1/party_1000/roles.json +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1002/party_500000/roles.json @@ -1,11 +1,10 @@ [ { "Type": "altinn", - "value": "REGNA" + "value": "A0237" }, { "Type": "altinn", - "value": "DAGL" + "value": "A0238" } ] - diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1002/party_510002/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1002/party_510002/roles.json new file mode 100644 index 000000000..e660cbeb0 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1002/party_510002/roles.json @@ -0,0 +1,16 @@ +[ + { + "Type": "altinn", + "value": "regna" + }, + { + "Type": "altinn", + "value": "dagl" + }, + { + "Type": "altinn", + "value": "priv" + } + ] + + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1003/party_510003/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1003/party_510003/roles.json new file mode 100644 index 000000000..e660cbeb0 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1003/party_510003/roles.json @@ -0,0 +1,16 @@ +[ + { + "Type": "altinn", + "value": "regna" + }, + { + "Type": "altinn", + "value": "dagl" + }, + { + "Type": "altinn", + "value": "priv" + } + ] + + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_12345/party_12345/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_12345/party_512345/roles.json similarity index 67% rename from test/Altinn.App.Api.Tests/Data/authorization/roles/User_12345/party_12345/roles.json rename to test/Altinn.App.Api.Tests/Data/authorization/roles/User_12345/party_512345/roles.json index c7f805fc1..d8d132a58 100644 --- a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_12345/party_12345/roles.json +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_12345/party_512345/roles.json @@ -6,6 +6,10 @@ { "Type": "altinn", "value": "dagl" + }, + { + "Type": "altinn", + "value": "priv" } ] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_1404/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_1404/roles.json deleted file mode 100644 index ebec433ce..000000000 --- a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_1404/roles.json +++ /dev/null @@ -1,67 +0,0 @@ -[ - { - "Type": "altinn", - "value": "PRIV" - }, - { - "Type": "altinn", - "value": "UTINN" - }, - { - "Type": "altinn", - "value": "LOPER" - } - , - { - "Type": "altinn", - "value": "ADMAI" - }, - { - "Type": "altinn", - "value": "PRIUT" - }, - { - "Type": "altinn", - "value": "REGNA" - }, - { - "Type": "altinn", - "value": "SISKD" - }, - { - "Type": "altinn", - "value": "UILUF" - }, - { - "Type": "altinn", - "value": "UTOMR" - }, - { - "Type": "altinn", - "value": "PAVAD" - }, - { - "Type": "altinn", - "value": "KOMAB" - }, - { - "Type": "altinn", - "value": "BOADM" - }, - { - "Type": "altinn", - "value": "A0212" - }, - { - "Type": "altinn", - "value": "A0236" - }, - { - "Type": "altinn", - "value": "A0278" - }, - { - "Type": "altinn", - "value": "A0282" - } -] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500000/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500000/roles.json index 22fefadd2..d68902de9 100644 --- a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500000/roles.json +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500000/roles.json @@ -1,7 +1,7 @@ [ - { - "Type": "altinn", - "value": "DAGL" + { + "Type": "altinn", + "value": "DAGL" }, { "Type": "altinn", @@ -11,9 +11,9 @@ "Type": "altinn", "value": "ADMAI" }, - { - "Type": "altinn", - "value": "REGNA" + { + "Type": "altinn", + "value": "REGNA" }, { "Type": "altinn", diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500800/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500800/roles.json new file mode 100644 index 000000000..b772f6fc0 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500800/roles.json @@ -0,0 +1,18 @@ +[ + { + "Type": "altinn", + "value": "MEDL" + }, + { + "Type": "altinn", + "value": "REGNA" + }, + { + "Type": "altinn", + "value": "UTINN" + }, + { + "Type": "altinn", + "value": "UTOMR" + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500801/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500801/roles.json new file mode 100644 index 000000000..b772f6fc0 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500801/roles.json @@ -0,0 +1,18 @@ +[ + { + "Type": "altinn", + "value": "MEDL" + }, + { + "Type": "altinn", + "value": "REGNA" + }, + { + "Type": "altinn", + "value": "UTINN" + }, + { + "Type": "altinn", + "value": "UTOMR" + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500802/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500802/roles.json new file mode 100644 index 000000000..b772f6fc0 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500802/roles.json @@ -0,0 +1,18 @@ +[ + { + "Type": "altinn", + "value": "MEDL" + }, + { + "Type": "altinn", + "value": "REGNA" + }, + { + "Type": "altinn", + "value": "UTINN" + }, + { + "Type": "altinn", + "value": "UTOMR" + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_1337/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_501337/roles.json similarity index 100% rename from test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_1337/roles.json rename to test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_501337/roles.json diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_1/party_1002/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/user_1/party_1002/roles.json deleted file mode 100644 index c7f805fc1..000000000 --- a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_1/party_1002/roles.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "Type": "altinn", - "value": "regna" - }, - { - "Type": "altinn", - "value": "dagl" - } -] - diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_2/party_1000/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/user_2/party_1000/roles.json deleted file mode 100644 index f50db8736..000000000 --- a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_2/party_1000/roles.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "Type": "altinn", - "value": "revai" - } -] - diff --git a/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs b/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs index e6b77da06..723fd6a3f 100644 --- a/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs +++ b/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs @@ -46,7 +46,7 @@ private static EformidlingStatusCheckEventHandler GetMockedEventHandler() var eFormidlingClientMock = new Mock(); var httpClientFactoryMock = new Mock(); var eFormidlingLoggerMock = new Mock>(); - + var httpClientMock = new Mock(); var maskinportenServiceLoggerMock = new Mock>(); var tokenCacheProviderMock = new Mock(); @@ -54,7 +54,7 @@ private static EformidlingStatusCheckEventHandler GetMockedEventHandler() var maskinportenSettingsMock = new Mock>(); var x509CertificateProviderMock = new Mock(); - IOptions platformSettingsMock = Options.Create(new Altinn.App.Core.Configuration.PlatformSettings() + IOptions platformSettingsMock = Options.Create(new Altinn.App.Core.Configuration.PlatformSettings() { ApiEventsEndpoint = "http://localhost:5101/events/api/v1/", SubscriptionKey = "key" @@ -70,7 +70,7 @@ private static EformidlingStatusCheckEventHandler GetMockedEventHandler() x509CertificateProviderMock.Object, platformSettingsMock, generalSettingsMock.Object - ); + ); return eventHandler; } } diff --git a/test/Altinn.App.Api.Tests/Mocks/AltinnPartyClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/AltinnPartyClientMock.cs new file mode 100644 index 000000000..3d8db7695 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Mocks/AltinnPartyClientMock.cs @@ -0,0 +1,45 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Core.Internal.Registers; +using Altinn.Platform.Register.Models; + +namespace Altinn.App.Api.Tests.Mocks; + +public class AltinnPartyClientMock : IAltinnPartyClient +{ + private readonly string _partyFolder = TestData.GetAltinnProfilePath(); + + public async Task GetParty(int partyId) + { + var file = Path.Join(_partyFolder, $"{partyId}.json"); + await using var fileHandle = File.OpenRead(file); // Throws exception if missing (helps with debugging tests) + return await JsonSerializer.DeserializeAsync(fileHandle, new JsonSerializerOptions(JsonSerializerDefaults.Web) + { + Converters = { new JsonStringEnumConverter() } + }); + } + + public async Task LookupParty(PartyLookup partyLookup) + { + var files = Directory.GetFiles(_partyFolder, "*.json"); + foreach (var file in files) + { + var fileHandle = File.OpenRead(file); + var party = (await JsonSerializer.DeserializeAsync(fileHandle))!; + if (partyLookup.OrgNo != null && party.OrgNumber == partyLookup.OrgNo) + { + return party; + } + + if (partyLookup.Ssn != null && party.SSN == partyLookup.Ssn) + { + return party; + } + } + + // Current implementation throws PlatformException if party is not found. Not sure what the correct behaviour for tests is. + throw new Exception( + $"Could not find party with orgNo {partyLookup.OrgNo} or ssn {partyLookup.Ssn} in {_partyFolder}"); + } +} diff --git a/test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs b/test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs index f07378265..95ebb5426 100644 --- a/test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs @@ -134,7 +134,7 @@ private static Application GetTestApplication(string org, string app) private static string GetMetadataPath() { - var uri = new Uri(typeof(InstanceMockSI).Assembly.Location); + var uri = new Uri(typeof(InstanceClientMockSi).Assembly.Location); string unitTestFolder = Path.GetDirectoryName(uri.LocalPath) ?? throw new Exception($"Unable to locate path {uri.LocalPath}"); return Path.Combine(unitTestFolder, @"../../../Data/Metadata"); diff --git a/test/Altinn.App.Api.Tests/Mocks/AppModelMock.cs b/test/Altinn.App.Api.Tests/Mocks/AppModelMock.cs new file mode 100644 index 000000000..54fcce581 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Mocks/AppModelMock.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using Altinn.App.Core.Internal.AppModel; + +namespace Altinn.App.Api.Tests.Mocks; + +public class AppModelMock: IAppModel +{ + public object Create(string classRef) + { + return Activator.CreateInstance(GetModelType(classRef))!; + } + + public Type GetModelType(string classRef) + { + // The default implementations uses the executing assembly, but this does not work in the test project. + return Assembly.GetAssembly(typeof(AppModelMock))!.GetType(classRef, true)!; + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs b/test/Altinn.App.Api.Tests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs index 6ff3aae21..2e3a9f2ce 100644 --- a/test/Altinn.App.Api.Tests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs +++ b/test/Altinn.App.Api.Tests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs @@ -14,7 +14,7 @@ namespace Altinn.App.Api.Tests.Mocks.Authentication public class JwtCookiePostConfigureOptionsStub : IPostConfigureOptions { /// - public void PostConfigure(string name, JwtCookieOptions options) + public void PostConfigure(string? name, JwtCookieOptions options) { if (string.IsNullOrEmpty(options.JwtCookieName)) { diff --git a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs index d0c40b1cb..bc843ee9f 100644 --- a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs @@ -1,12 +1,12 @@ -using Altinn.App.Core.Interface; -using Altinn.Platform.Register.Models; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; +using Altinn.Platform.Register.Models; +using System.Security.Claims; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Api.Tests.Mocks { - public class AuthorizationMock : IAuthorization + public class AuthorizationMock : IAuthorizationClient { public Task?> GetPartyList(int userId) { @@ -15,14 +15,49 @@ public class AuthorizationMock : IAuthorization public Task ValidateSelectedParty(int userId, int partyId) { - bool? isvalid = true; + bool? isvalid = userId != 1; - if (userId == 1) + return Task.FromResult(isvalid); + } + + /// + /// Mock method that returns false for actions ending with _unauthorized, and true for all other actions. + /// + /// + /// + /// + /// + /// + /// + /// + public async Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, ClaimsPrincipal user, string action, string? taskId = null) + { + await Task.CompletedTask; + if(action.EndsWith("_unauthorized")) { - isvalid = false; + return false; } - return Task.FromResult(isvalid); + return true; + } + + public async Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions) + { + await Task.CompletedTask; + Dictionary authorizedActions = new Dictionary(); + foreach (var action in actions) + { + if(action.EndsWith("_unauthorized")) + { + authorizedActions.Add(action, false); + } + else + { + authorizedActions.Add(action, true); + } + } + + return authorizedActions; } } } diff --git a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs index 7f47b9922..4e591793d 100644 --- a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs @@ -2,8 +2,8 @@ using System.Xml.Serialization; using Altinn.App.Api.Tests.Data; using Altinn.App.Core.Extensions; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; @@ -11,7 +11,7 @@ namespace App.IntegrationTests.Mocks.Services { - public class DataClientMock : IData + public class DataClientMock : IDataClient { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IAppMetadata _appMetadata; @@ -197,7 +197,7 @@ public Task UpdateData(T dataToSerialize, Guid instanceGuid, Typ serializer.Serialize(stream, dataToSerialize); } - dataElement.LastChanged = DateTime.Now; + dataElement.LastChanged = DateTime.UtcNow; WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId); return Task.FromResult(dataElement); @@ -280,6 +280,46 @@ public Task UpdateBinaryData(InstanceIdentifier instanceIdentifier, throw new NotImplementedException(); } + public async Task InsertBinaryData(string instanceId, string dataType, string contentType, string filename, Stream stream, string? generatedFromTask = null) + { + Application application = await _appMetadata.GetApplicationMetadata(); + var instanceIdParts = instanceId.Split("/"); + + Guid dataGuid = Guid.NewGuid(); + + string org = application.Org; + string app = application.Id.Split("/")[1]; + int instanceOwnerId = int.Parse(instanceIdParts[0]); + Guid instanceGuid = Guid.Parse(instanceIdParts[1]); + + string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerId, instanceGuid); + + DataElement dataElement = new() { Id = dataGuid.ToString(), InstanceGuid = instanceGuid.ToString(), DataType = dataType, ContentType = contentType, }; + + if (!Directory.Exists(Path.GetDirectoryName(dataPath))) + { + var directory = Path.GetDirectoryName(dataPath); + if (directory != null) Directory.CreateDirectory(directory); + } + + Directory.CreateDirectory(dataPath + @"blob"); + + long filesize; + + using (Stream streamToWriteTo = File.Open(dataPath + @"blob/" + dataGuid, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + stream.Seek(0, SeekOrigin.Begin); + await stream.CopyToAsync(streamToWriteTo); + streamToWriteTo.Flush(); + filesize = streamToWriteTo.Length; + } + + dataElement.Size = filesize; + WriteDataElementToFile(dataElement, org, app, instanceOwnerId); + + return dataElement; + } + public Task UpdateBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid, HttpRequest request) { throw new NotImplementedException(); @@ -296,6 +336,16 @@ public Task Update(Instance instance, DataElement dataElement) return Task.FromResult(dataElement); } + public Task LockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + { + throw new NotImplementedException(); + } + + public Task UnlockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + { + throw new NotImplementedException(); + } + private static void WriteDataElementToFile(DataElement dataElement, string org, string app, int instanceOwnerPartyId) { string dataElementPath = TestData.GetDataElementPath(org, app, instanceOwnerPartyId, Guid.Parse(dataElement.InstanceGuid), Guid.Parse(dataElement.Id)); diff --git a/test/Altinn.App.Api.Tests/Mocks/Event/InstanceEventClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/Event/InstanceEventClientMock.cs new file mode 100644 index 000000000..920d80a2e --- /dev/null +++ b/test/Altinn.App.Api.Tests/Mocks/Event/InstanceEventClientMock.cs @@ -0,0 +1,18 @@ +using Altinn.App.Core.Internal.Instances; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Api.Tests.Mocks.Event; + +public class InstanceEventClientMock : IInstanceEventClient +{ + public Task SaveInstanceEvent(object dataToSerialize, string org, string app) + { + return Task.FromResult(Guid.NewGuid().ToString()); + } + + public Task> GetInstanceEvents(string instanceId, string instanceOwnerPartyId, string org, string app, string[] eventTypes, + string from, string to) + { + throw new NotImplementedException(); + } +} diff --git a/test/Altinn.App.Api.Tests/Mocks/InstanceMockSI.cs b/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs similarity index 98% rename from test/Altinn.App.Api.Tests/Mocks/InstanceMockSI.cs rename to test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs index 4674456ad..99dc9d0e2 100644 --- a/test/Altinn.App.Api.Tests/Mocks/InstanceMockSI.cs +++ b/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs @@ -1,21 +1,21 @@ using Altinn.App.Api.Tests.Data; using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; +using Altinn.App.Core.Internal.Instances; namespace Altinn.App.Api.Tests.Mocks { - public class InstanceMockSI : IInstance + public class InstanceClientMockSi : IInstanceClient { private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; - public InstanceMockSI(ILogger logger, IHttpContextAccessor httpContextAccessor) + public InstanceClientMockSi(ILogger logger, IHttpContextAccessor httpContextAccessor) { _logger = logger; _httpContextAccessor = httpContextAccessor; @@ -419,7 +419,7 @@ public async Task> GetInstances(Dictionary if (queryParams.TryGetValue("appId", out StringValues appIdQueryVal) && appIdQueryVal.Count > 0) { - instancesPath += Path.DirectorySeparatorChar + appIdQueryVal.First().Replace('/', Path.DirectorySeparatorChar); + instancesPath += Path.DirectorySeparatorChar + appIdQueryVal.First()?.Replace('/', Path.DirectorySeparatorChar); fileDepth -= 2; if (queryParams.TryGetValue("instanceOwner.partyId", out StringValues partyIdQueryVal) && partyIdQueryVal.Count > 0) diff --git a/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs b/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs index 18fe1f030..bb0100c59 100644 --- a/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs +++ b/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs @@ -1,5 +1,4 @@ -using Altinn.App.Core.Interface; -using Altinn.Authorization.ABAC.Constants; +using Altinn.Authorization.ABAC.Constants; using Altinn.Authorization.ABAC.Utils; using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Authorization.ABAC.Xacml; @@ -9,22 +8,18 @@ using Authorization.Platform.Authorization.Models; using Microsoft.Extensions.Options; using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Security.Claims; -using System.Threading.Tasks; using System.Xml; using Altinn.App.Api.Tests.Models; using Altinn.App.Api.Tests.Constants; using Altinn.App.Api.Tests.Data; +using Altinn.App.Core.Internal.Instances; namespace Altinn.App.Api.Tests.Mocks { public class PepWithPDPAuthorizationMockSI : Common.PEP.Interfaces.IPDP { - private readonly IInstance _instanceService; + private readonly IInstanceClient _instanceClient; private readonly PepSettings _pepSettings; @@ -44,9 +39,9 @@ public class PepWithPDPAuthorizationMockSI : Common.PEP.Interfaces.IPDP private const string AltinnRoleAttributeId = "urn:altinn:rolecode"; - public PepWithPDPAuthorizationMockSI(IInstance instanceService, IOptions pepSettings) + public PepWithPDPAuthorizationMockSI(IInstanceClient instanceClient, IOptions pepSettings) { - this._instanceService = instanceService; + this._instanceClient = instanceClient; _pepSettings = pepSettings.Value; } @@ -66,7 +61,7 @@ public async Task GetDecisionForRequest(XacmlJsonRequestRoot } catch { - return null; + return null!; } } @@ -121,7 +116,7 @@ private async Task EnrichResourceAttributes(XacmlContextRequest request) if (!resourceAttributeComplete) { - Instance instanceData = await _instanceService.GetInstance(resourceAttributes.AppValue, resourceAttributes.OrgValue, Convert.ToInt32(resourceAttributes.InstanceValue.Split('/')[0]), new Guid(resourceAttributes.InstanceValue.Split('/')[1])); + Instance instanceData = await _instanceClient.GetInstance(resourceAttributes.AppValue, resourceAttributes.OrgValue, Convert.ToInt32(resourceAttributes.InstanceValue.Split('/')[0]), new Guid(resourceAttributes.InstanceValue.Split('/')[1])); if (instanceData != null) { diff --git a/test/Altinn.App.Api.Tests/Mocks/ProfileClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/ProfileClientMock.cs new file mode 100644 index 000000000..583608eb2 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Mocks/ProfileClientMock.cs @@ -0,0 +1,20 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Core.Internal.Profile; +using Altinn.Platform.Profile.Models; + +namespace Altinn.App.Api.Tests.Mocks; + +public class ProfileClientMock : IProfileClient +{ + public async Task GetUserProfile(int userId) + { + var folder = TestData.GetRegisterProfilePath(); + var file = Path.Join(folder, $"{userId}.json"); + return (await JsonSerializer.DeserializeAsync(File.OpenRead(file), new JsonSerializerOptions(JsonSerializerDefaults.Web) + { + Converters = { new JsonStringEnumConverter() } + }))!; + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Program.cs b/test/Altinn.App.Api.Tests/Program.cs index eb36a4568..8510f006e 100644 --- a/test/Altinn.App.Api.Tests/Program.cs +++ b/test/Altinn.App.Api.Tests/Program.cs @@ -1,16 +1,21 @@ using Altinn.App.Api.Extensions; +using Altinn.App.Api.Tests.Data; using Altinn.App.Api.Tests.Mocks; using Altinn.App.Api.Tests.Mocks.Authentication; using Altinn.App.Api.Tests.Mocks.Event; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Events; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using AltinnCore.Authentication.JwtCookie; using App.IntegrationTests.Mocks.Services; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -23,6 +28,9 @@ // external api's etc. should be mocked. WebApplicationBuilder builder = WebApplication.CreateBuilder(new WebApplicationOptions() { ApplicationName = "Altinn.App.Api.Tests" }); +builder.Configuration.AddJsonFile(Path.Join(TestData.GetTestDataRootDirectory(), "apps", "tdd", "contributer-restriction", "appsettings.json")); +builder.Configuration.GetSection("MetricsSettings:Enabled").Value = "false"; + ConfigureServices(builder.Services, builder.Configuration); ConfigureMockServices(builder.Services, builder.Configuration); @@ -40,8 +48,8 @@ void ConfigureMockServices(IServiceCollection services, ConfigurationManager con { PlatformSettings platformSettings = new PlatformSettings() { ApiAuthorizationEndpoint = "http://localhost:5101/authorization/api/v1/" }; services.AddSingleton>(Options.Create(platformSettings)); - services.AddTransient(); - services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton, JwtCookiePostConfigureOptionsStub>(); @@ -50,27 +58,16 @@ void ConfigureMockServices(IServiceCollection services, ConfigurationManager con services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } void Configure() { - if (app.Environment.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseDefaultSecurityHeaders(); - app.UseRouting(); - app.UseStaticFiles(); - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - app.UseHealthChecks("/health"); + app.UseAltinnAppCommonConfiguration(); } // This "hack" (documentet by Microsoft) is done to diff --git a/test/Altinn.App.Api.Tests/TestResources/Altinn.App.Api.xml b/test/Altinn.App.Api.Tests/TestResources/Altinn.App.Api.xml index e10f6ce62..ffadad03c 100644 --- a/test/Altinn.App.Api.Tests/TestResources/Altinn.App.Api.xml +++ b/test/Altinn.App.Api.Tests/TestResources/Altinn.App.Api.xml @@ -103,7 +103,7 @@ Exposes API endpoints related to authentication. - + Initializes a new instance of the class @@ -125,7 +125,7 @@ Exposes API endpoints related to authorization - + Initializes a new instance of the class @@ -149,7 +149,7 @@ The data controller handles creation, update, validation and calculation of data elements. - + The data controller is responsible for adding business logic to the data elements. @@ -222,7 +222,7 @@ This controller class provides action methods for endpoints related to the tags resource on data elements. - + Initialize a new instance of with the given services. @@ -309,7 +309,7 @@ You can create a new instance (POST), update it (PUT) and retrieve a specific instance (GET). - + Initializes a new instance of the class @@ -455,7 +455,7 @@ Handles party related operations - + Initializes a new instance of the class @@ -489,7 +489,7 @@ Controller for setting and moving process flow of an instance. - + Initializes a new instance of the @@ -560,7 +560,7 @@ Controller that exposes profile - + Initializes a new instance of the class @@ -702,7 +702,7 @@ The stateless data controller handles creation and calculation of data elements not related to an instance. - + The stateless data controller is responsible for creating and updating stateles data elements. @@ -783,7 +783,7 @@ Represents all actions related to validation of data and instances - + Initialises a new instance of the class diff --git a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs b/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs index 2a86d6c77..94280afcd 100644 --- a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs +++ b/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs @@ -7,21 +7,30 @@ namespace Altinn.App.Api.Tests.Utils { public static class PrincipalUtil { - public static string GetToken(int userId, int authenticationLevel = 2) + public static string GetToken(int? userId, int? partyId, int authenticationLevel = 2) { - ClaimsPrincipal principal = GetUserPrincipal(userId, authenticationLevel); + ClaimsPrincipal principal = GetUserPrincipal(userId, partyId, authenticationLevel); string token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); return token; } - public static ClaimsPrincipal GetUserPrincipal(int userId, int authenticationLevel = 2) + public static ClaimsPrincipal GetUserPrincipal(int? userId, int? partyId, int authenticationLevel = 2) { List claims = new List(); string issuer = "www.altinn.no"; - claims.Add(new Claim(ClaimTypes.NameIdentifier, userId.ToString(), ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.UserId, userId.ToString(), ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.UserName, "UserOne", ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.PartyID, userId.ToString(), ClaimValueTypes.Integer32, issuer)); + + claims.Add(new Claim(ClaimTypes.NameIdentifier, $"user-{userId}-{partyId}", ClaimValueTypes.String, issuer)); + if (userId > 0) + { + claims.Add(new Claim(AltinnCoreClaimTypes.UserId, userId.ToString()!, ClaimValueTypes.String, issuer)); + } + + if (partyId > 0) + { + claims.Add(new Claim(AltinnCoreClaimTypes.PartyID, userId.ToString()!, ClaimValueTypes.Integer32, issuer)); + } + + claims.Add(new Claim(AltinnCoreClaimTypes.UserName, $"User{userId}", ClaimValueTypes.String, issuer)); claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticationLevel, authenticationLevel.ToString(), ClaimValueTypes.Integer32, issuer)); diff --git a/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj index 179391b37..e0e02bfae 100644 --- a/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj +++ b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj @@ -5,6 +5,12 @@ enable false + + $(NoWarn);CS1591;CS0618 + diff --git a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj index 4149690fc..02e1881bd 100644 --- a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj +++ b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj @@ -1,18 +1,27 @@  - net6.0 + net8.0 false Altinn.App.Core.Tests enable + true + + $(NoWarn);CS1591;CS0618 + + + @@ -28,6 +37,7 @@ Always + @@ -61,7 +71,10 @@ - + + Always + + Always @@ -73,6 +86,9 @@ Always + + Always + Always @@ -88,13 +104,13 @@ + + + + ..\..\Altinn3.ruleset - - true - $(NoWarn);1591 - diff --git a/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs b/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs index 17b996cc7..fca68fbe6 100644 --- a/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs +++ b/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs @@ -12,12 +12,13 @@ namespace Altinn.App.PlatformServices.Tests.DataLists public class NullDataListProviderTest { [Fact] - public void Constructor_InitializedWithEmptyValues() + public async Task Constructor_InitializedWithEmptyValues() { var provider = new NullDataListProvider(); provider.Id.Should().Be(string.Empty); - provider.GetDataListAsync("nb", new Dictionary()).Result.ListItems.Should().BeNull(); + var list = await provider.GetDataListAsync("nb", new Dictionary()); + list.ListItems.Should().BeNull(); } } } \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs b/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs new file mode 100644 index 000000000..82b23e264 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs @@ -0,0 +1,220 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Constants; +using Altinn.App.Core.EFormidling; +using Altinn.App.Core.EFormidling.Implementation; +using Altinn.App.Core.EFormidling.Interface; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Events; +using Altinn.App.Core.Models; +using Altinn.Common.AccessTokenClient.Services; +using Altinn.Common.EFormidlingClient; +using Altinn.Common.EFormidlingClient.Models.SBD; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Eformidling.Implementation; + +public class DefaultEFormidlingServiceTests +{ + [Fact] + public void SendEFormidlingShipment() + { + // Arrange + var logger = new NullLogger(); + var userTokenProvider = new Mock(); + var appMetadata = new Mock(); + var dataClient = new Mock(); + var eFormidlingReceivers = new Mock(); + var eventClient = new Mock(); + var appSettings = Options.Create(new AppSettings + { + RuntimeCookieName = "AltinnStudioRuntime", + EFormidlingSender = "980123456", + }); + var platformSettings = Options.Create(new PlatformSettings + { + SubscriptionKey = "subscription-key" + }); + var eFormidlingClient = new Mock(); + var tokenGenerator = new Mock(); + var eFormidlingMetadata = new Mock(); + var instance = new Instance + { + Id = "1337/41C1099C-7EDD-47F5-AD1F-6267B497796F", + InstanceOwner = new InstanceOwner + { + PartyId = "1337", + }, + Data = new List() + }; + + appMetadata.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(new ApplicationMetadata("ttd/test-app") + { + Org = "ttd", + EFormidling = new EFormidlingContract + { + Process = "urn:no:difi:profile:arkivmelding:plan:3.0", + Standard = "urn:no:difi:arkivmelding:xsd::arkivmelding", + TypeVersion = "v8", + Type = "arkivmelding", + SecurityLevel = 3, + DataTypes = new List() + } + }); + tokenGenerator.Setup(t => t.GenerateAccessToken("ttd", "test-app")).Returns("access-token"); + userTokenProvider.Setup(u => u.GetUserToken()).Returns("authz-token"); + eFormidlingReceivers.Setup(er => er.GetEFormidlingReceivers(instance)).ReturnsAsync(new List()); + eFormidlingMetadata.Setup(em => em.GenerateEFormidlingMetadata(instance)).ReturnsAsync(() => + { + return ("fakefilename.txt", Stream.Null); + }); + + var defaultEformidlingService = new DefaultEFormidlingService( + logger, + userTokenProvider.Object, + appMetadata.Object, + dataClient.Object, + eFormidlingReceivers.Object, + eventClient.Object, + appSettings, + platformSettings, + eFormidlingClient.Object, + tokenGenerator.Object, + eFormidlingMetadata.Object); + + // Act + var result = defaultEformidlingService.SendEFormidlingShipment(instance); + + // Assert + var expectedReqHeaders = new Dictionary + { + { "Authorization", $"Bearer authz-token" }, + { General.EFormidlingAccessTokenHeaderName, "access-token" }, + { General.SubscriptionKeyHeaderName, "subscription-key" } + }; + + appMetadata.Verify(a => a.GetApplicationMetadata()); + tokenGenerator.Verify(t => t.GenerateAccessToken("ttd", "test-app")); + userTokenProvider.Verify(u => u.GetUserToken()); + eFormidlingReceivers.Verify(er => er.GetEFormidlingReceivers(instance)); + eFormidlingMetadata.Verify(em => em.GenerateEFormidlingMetadata(instance)); + eFormidlingClient.Verify(ec => ec.CreateMessage(It.IsAny(), expectedReqHeaders)); + eFormidlingClient.Verify(ec => ec.UploadAttachment(Stream.Null, "41C1099C-7EDD-47F5-AD1F-6267B497796F", "fakefilename.txt", expectedReqHeaders)); + eFormidlingClient.Verify(ec => ec.SendMessage("41C1099C-7EDD-47F5-AD1F-6267B497796F", expectedReqHeaders)); + eventClient.Verify(e => e.AddEvent(EformidlingConstants.CheckInstanceStatusEventType, instance)); + + eFormidlingClient.VerifyNoOtherCalls(); + eventClient.VerifyNoOtherCalls(); + tokenGenerator.VerifyNoOtherCalls(); + userTokenProvider.VerifyNoOtherCalls(); + eFormidlingReceivers.VerifyNoOtherCalls(); + appMetadata.VerifyNoOtherCalls(); + + result.IsCompletedSuccessfully.Should().BeTrue(); + } + + [Fact] + public void SendEFormidlingShipment_throws_exception_if_send_fails() + { + // Arrange + var logger = new NullLogger(); + var userTokenProvider = new Mock(); + var appMetadata = new Mock(); + var dataClient = new Mock(); + var eFormidlingReceivers = new Mock(); + var eventClient = new Mock(); + var appSettings = Options.Create(new AppSettings + { + RuntimeCookieName = "AltinnStudioRuntime", + EFormidlingSender = "980123456", + }); + var platformSettings = Options.Create(new PlatformSettings + { + SubscriptionKey = "subscription-key", + }); + var eFormidlingClient = new Mock(); + var tokenGenerator = new Mock(); + var eFormidlingMetadata = new Mock(); + var instance = new Instance + { + Id = "1337/41C1099C-7EDD-47F5-AD1F-6267B497796F", + InstanceOwner = new InstanceOwner + { + PartyId = "1337", + }, + Data = new List() + }; + + appMetadata.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(new ApplicationMetadata("ttd/test-app") + { + Org = "ttd", + EFormidling = new EFormidlingContract + { + Process = "urn:no:difi:profile:arkivmelding:plan:3.0", + Standard = "urn:no:difi:arkivmelding:xsd::arkivmelding", + TypeVersion = "v8", + Type = "arkivmelding", + SecurityLevel = 3, + DataTypes = new List() + } + }); + tokenGenerator.Setup(t => t.GenerateAccessToken("ttd", "test-app")).Returns("access-token"); + userTokenProvider.Setup(u => u.GetUserToken()).Returns("authz-token"); + eFormidlingReceivers.Setup(er => er.GetEFormidlingReceivers(instance)).ReturnsAsync(new List()); + eFormidlingMetadata.Setup(em => em.GenerateEFormidlingMetadata(instance)).ReturnsAsync(() => + { + return ("fakefilename.txt", Stream.Null); + }); + eFormidlingClient.Setup(ec => ec.SendMessage(It.IsAny(), It.IsAny>())) + .ThrowsAsync(new Exception("XUnit expected exception")); + + var defaultEformidlingService = new DefaultEFormidlingService( + logger, + userTokenProvider.Object, + appMetadata.Object, + dataClient.Object, + eFormidlingReceivers.Object, + eventClient.Object, + appSettings, + platformSettings, + eFormidlingClient.Object, + tokenGenerator.Object, + eFormidlingMetadata.Object); + + // Act + var result = defaultEformidlingService.SendEFormidlingShipment(instance); + + // Assert + // Assert + var expectedReqHeaders = new Dictionary + { + { "Authorization", $"Bearer authz-token" }, + { General.EFormidlingAccessTokenHeaderName, "access-token" }, + { General.SubscriptionKeyHeaderName, "subscription-key" } + }; + + appMetadata.Verify(a => a.GetApplicationMetadata()); + tokenGenerator.Verify(t => t.GenerateAccessToken("ttd", "test-app")); + userTokenProvider.Verify(u => u.GetUserToken()); + eFormidlingReceivers.Verify(er => er.GetEFormidlingReceivers(instance)); + eFormidlingMetadata.Verify(em => em.GenerateEFormidlingMetadata(instance)); + eFormidlingClient.Verify(ec => ec.CreateMessage(It.IsAny(), expectedReqHeaders)); + eFormidlingClient.Verify(ec => ec.UploadAttachment(Stream.Null, "41C1099C-7EDD-47F5-AD1F-6267B497796F", "fakefilename.txt", expectedReqHeaders)); + eFormidlingClient.Verify(ec => ec.SendMessage("41C1099C-7EDD-47F5-AD1F-6267B497796F", expectedReqHeaders)); + + eFormidlingClient.VerifyNoOtherCalls(); + eventClient.VerifyNoOtherCalls(); + tokenGenerator.VerifyNoOtherCalls(); + userTokenProvider.VerifyNoOtherCalls(); + eFormidlingReceivers.VerifyNoOtherCalls(); + appMetadata.VerifyNoOtherCalls(); + + result.IsCompletedSuccessfully.Should().BeFalse(); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs b/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs index 4da0bab88..dd7298272 100644 --- a/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs +++ b/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs @@ -21,7 +21,7 @@ public void ToNameValueString_OptionParameters_ShouldConvertToHttpHeaderFormat() IHeaderDictionary headers = new HeaderDictionary { - { "Altinn-DownstreamParameters", options.Parameters.ToNameValueString(',') } + { "Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',') } }; Assert.Equal("lang=nb,level=1", headers["Altinn-DownstreamParameters"]); @@ -37,7 +37,7 @@ public void ToNameValueString_OptionParametersWithEmptyValue_ShouldConvertToHttp IHeaderDictionary headers = new HeaderDictionary { - { "Altinn-DownstreamParameters", options.Parameters.ToNameValueString(',') } + { "Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',') } }; Assert.Equal(string.Empty, headers["Altinn-DownstreamParameters"]); @@ -48,15 +48,35 @@ public void ToNameValueString_OptionParametersWithNullValue_ShouldConvertToHttpH { var options = new AppOptions { - Parameters = null + Parameters = null! }; IHeaderDictionary headers = new HeaderDictionary { - { "Altinn-DownstreamParameters", options.Parameters.ToNameValueString(',') } + { "Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',') } }; Assert.Equal(string.Empty, headers["Altinn-DownstreamParameters"]); } + + [Fact] + public void ToNameValueString_OptionParametersWithSpecialCharaters_IsValidAsHeaders() + { + var options = new AppOptions + { + Parameters = new Dictionary + { + { "lang", "nb" }, + { "level", "1" }, + { "name", "ÆØÅ" }, + { "variant", "SmÃ¥vilt1" } + }, + }; + + IHeaderDictionary headers = new HeaderDictionary(); + headers.Add("Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',')); + + Assert.Equal("lang=nb,level=1,name=%C3%86%C3%98%C3%85,variant=Sm%C3%A5vilt1", headers["Altinn-DownstreamParameters"]); + } } } diff --git a/test/Altinn.App.Core.Tests/Extensions/InstanceEventExtensionsTests.cs b/test/Altinn.App.Core.Tests/Extensions/InstanceEventExtensionsTests.cs new file mode 100644 index 000000000..08640c8d4 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Extensions/InstanceEventExtensionsTests.cs @@ -0,0 +1,215 @@ +using Altinn.App.Core.Extensions; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Extensions; + +public class InstanceEventExtensionsTests +{ + [Fact] + public void CopyValues_returns_copy_of_instance_event() + { + InstanceEvent original = new InstanceEvent() + { + Created = DateTime.Now, + DataId = Guid.NewGuid().ToString(), + EventType = "EventType", + Id = Guid.NewGuid(), + InstanceId = Guid.NewGuid().ToString(), + InstanceOwnerPartyId = "1", + ProcessInfo = new ProcessState + { + Started = DateTime.Now, + CurrentTask = new ProcessElementInfo + { + Flow = 1, + AltinnTaskType = "AltinnTaskType", + ElementId = "ElementId", + Name = "Name", + Started = DateTime.Now, + Ended = DateTime.Now, + Validated = new ValidationStatus + { + CanCompleteTask = true, + Timestamp = DateTime.Now + } + }, + StartEvent = "StartEvent" + }, + User = new PlatformUser + { + AuthenticationLevel = 2, + EndUserSystemId = 1, + OrgId = "OrgId", + UserId = 3, + NationalIdentityNumber = "NationalIdentityNumber" + } + }; + InstanceEvent copy = original.CopyValues(); + copy.Should().NotBeSameAs(original); + copy.ProcessInfo.Should().NotBeSameAs(original.ProcessInfo); + copy.ProcessInfo.CurrentTask.Should().NotBeSameAs(original.ProcessInfo.CurrentTask); + copy.ProcessInfo.CurrentTask.Validated.Should().NotBeSameAs(original.ProcessInfo.CurrentTask.Validated); + copy.User.Should().NotBeSameAs(original.User); + copy.Should().BeEquivalentTo(original); + } + + [Fact] + public void CopyValues_returns_copy_of_instance_event_Validated_null() + { + Guid id = Guid.NewGuid(); + string dataGuid = Guid.NewGuid().ToString(); + string instanceGuid = Guid.NewGuid().ToString(); + DateTime now = DateTime.Now; + InstanceEvent original = new InstanceEvent() + { + Created = now, + DataId = dataGuid, + EventType = "EventType", + Id = id, + InstanceId = instanceGuid, + InstanceOwnerPartyId = "1", + ProcessInfo = new ProcessState + { + Started = now, + CurrentTask = new ProcessElementInfo + { + Flow = 1, + AltinnTaskType = "AltinnTaskType", + ElementId = "ElementId", + Name = "Name", + Started = now, + Ended = now, + Validated = null + }, + StartEvent = "StartEvent" + }, + User = new PlatformUser + { + AuthenticationLevel = 2, + EndUserSystemId = 1, + OrgId = "OrgId", + UserId = 3, + NationalIdentityNumber = "NationalIdentityNumber" + } + }; + InstanceEvent expected = new InstanceEvent() + { + Created = now, + DataId = dataGuid, + EventType = "EventType", + Id = id, + InstanceId = instanceGuid, + InstanceOwnerPartyId = "1", + ProcessInfo = new ProcessState + { + Started = now, + CurrentTask = new ProcessElementInfo + { + Flow = 1, + AltinnTaskType = "AltinnTaskType", + ElementId = "ElementId", + Name = "Name", + Started = now, + Ended = now, + Validated = new() + { + Timestamp = null, + CanCompleteTask = false + } + }, + StartEvent = "StartEvent" + }, + User = new PlatformUser + { + AuthenticationLevel = 2, + EndUserSystemId = 1, + OrgId = "OrgId", + UserId = 3, + NationalIdentityNumber = "NationalIdentityNumber" + } + }; + InstanceEvent copy = original.CopyValues(); + copy.Should().NotBeSameAs(original); + copy.ProcessInfo.Should().NotBeSameAs(original.ProcessInfo); + copy.ProcessInfo.CurrentTask.Should().NotBeSameAs(original.ProcessInfo.CurrentTask); + copy.ProcessInfo.CurrentTask.Validated.Should().NotBeSameAs(original.ProcessInfo.CurrentTask.Validated); + copy.User.Should().NotBeSameAs(original.User); + copy.Should().BeEquivalentTo(expected); + } + + [Fact] + public void CopyValues_returns_copy_of_instance_event_CurrentTask_null() + { + Guid id = Guid.NewGuid(); + string dataGuid = Guid.NewGuid().ToString(); + string instanceGuid = Guid.NewGuid().ToString(); + DateTime now = DateTime.Now; + InstanceEvent original = new InstanceEvent() + { + Created = now, + DataId = dataGuid, + EventType = "EventType", + Id = id, + InstanceId = instanceGuid, + InstanceOwnerPartyId = "1", + ProcessInfo = new ProcessState + { + Started = now, + CurrentTask = null, + StartEvent = "StartEvent" + }, + User = new PlatformUser + { + AuthenticationLevel = 2, + EndUserSystemId = 1, + OrgId = "OrgId", + UserId = 3, + NationalIdentityNumber = "NationalIdentityNumber" + } + }; + InstanceEvent expected = new InstanceEvent() + { + Created = now, + DataId = dataGuid, + EventType = "EventType", + Id = id, + InstanceId = instanceGuid, + InstanceOwnerPartyId = "1", + ProcessInfo = new ProcessState + { + Started = now, + CurrentTask = new ProcessElementInfo + { + Flow = null, + AltinnTaskType = null, + ElementId = null, + Name = null, + Started = null, + Ended = null, + Validated = new() + { + Timestamp = null, + CanCompleteTask = false + } + }, + StartEvent = "StartEvent" + }, + User = new PlatformUser + { + AuthenticationLevel = 2, + EndUserSystemId = 1, + OrgId = "OrgId", + UserId = 3, + NationalIdentityNumber = "NationalIdentityNumber" + } + }; + InstanceEvent copy = original.CopyValues(); + copy.Should().NotBeSameAs(original); + copy.ProcessInfo.Should().NotBeSameAs(original.ProcessInfo); + copy.ProcessInfo.CurrentTask.Should().NotBeSameAs(original.ProcessInfo.CurrentTask); + copy.User.Should().NotBeSameAs(original.User); + copy.Should().BeEquivalentTo(expected); + } +} diff --git a/test/Altinn.App.Core.Tests/Extensions/ProcessStateExtensionTests.cs b/test/Altinn.App.Core.Tests/Extensions/ProcessStateExtensionTests.cs new file mode 100644 index 000000000..0e388beaf --- /dev/null +++ b/test/Altinn.App.Core.Tests/Extensions/ProcessStateExtensionTests.cs @@ -0,0 +1,67 @@ +using Altinn.App.Core.Extensions; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Extensions; + +public class ProcessStateExtensionTests +{ + [Fact] + public void Copy_returns_copy_of_process_state() + { + ProcessState original = new ProcessState(); + ProcessState copy = original.Copy(); + Assert.NotSame(original, copy); + } + + [Fact] + public void Copy_returns_state_with_fields_set() + { + ProcessState original = new ProcessState() + { + Ended = DateTime.Now, + Started = DateTime.Now, + StartEvent = "StartEvent", + EndEvent = "EndEvent", + CurrentTask = new ProcessElementInfo() + { + Ended = DateTime.Now, + Started = DateTime.Now, + ElementId = "ElementId", + AltinnTaskType = "AltinnTaskType", + Flow = 1, + FlowType = "FlowType", + Name = "Name", + Validated = new ValidationStatus() + { + Timestamp = DateTime.Now, + CanCompleteTask = true + }, + } + }; + ProcessState copy = original.Copy(); + Assert.NotSame(original, copy); + Assert.NotSame(original.CurrentTask, copy.CurrentTask); + Assert.Same(original.CurrentTask.Validated, copy.CurrentTask.Validated); + copy.Should().BeEquivalentTo(original); + } + + [Fact] + public void Copy_returns_state_with_current_null_when_original_null() + { + ProcessState original = new ProcessState() + { + Ended = DateTime.Now, + Started = DateTime.Now, + StartEvent = "StartEvent", + EndEvent = "EndEvent", + CurrentTask = null + }; + ProcessState copy = original.Copy(); + Assert.NotSame(original, copy); + Assert.Same(original.CurrentTask, copy.CurrentTask); + copy.Should().BeEquivalentTo(original); + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs new file mode 100644 index 000000000..197b00cc0 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs @@ -0,0 +1,131 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Sign; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.UserAction; +using Altinn.App.Core.Tests.Internal.Process.TestUtils; +using Altinn.Platform.Profile.Models; +using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.FeatureManagement; +using Moq; +using Xunit; +using Signee = Altinn.App.Core.Internal.Sign.Signee; + +namespace Altinn.App.Core.Tests.Features.Action; + +public class SigningUserActionTests +{ + [Fact] + public async void HandleAction_returns_ok_if_user_is_valid() + { + // Arrange + UserProfile userProfile = new UserProfile() + { + UserId = 1337, + Party = new Party() { SSN = "12345678901" } + }; + (var userAction, var signClientMock) = CreateSigningUserAction(userProfile); + var instance = new Instance() + { + Id = "500000/b194e9f5-02d0-41bc-8461-a0cbac8a6efc", + InstanceOwner = new() + { + PartyId = "5000", + }, + Process = new() + { + CurrentTask = new() + { + ElementId = "Task2" + } + }, + Data = new() + { + new() + { + Id = "a499c3ef-e88a-436b-8650-1c43e5037ada", + DataType = "Model" + } + } + }; + var userActionContext = new UserActionContext(instance, 1337); + + // Act + var result = await userAction.HandleAction(userActionContext); + + // Assert + SignatureContext expected = new SignatureContext(new InstanceIdentifier(instance), "signature", new Signee() { UserId = "1337", PersonNumber = "12345678901" }, new DataElementSignature("a499c3ef-e88a-436b-8650-1c43e5037ada")); + signClientMock.Verify(s => s.SignDataElements(It.Is(sc => AssertSigningContextAsExpected(sc, expected))), Times.Once); + result.Should().BeEquivalentTo(UserActionResult.SuccessResult()); + signClientMock.VerifyNoOtherCalls(); + } + + [Fact] + public async void HandleAction_throws_ApplicationConfigException_if_SignatureDataType_is_null() + { + // Arrange + UserProfile userProfile = new UserProfile() + { + UserId = 1337, + Party = new Party() { SSN = "12345678901" } + }; + (var userAction, var signClientMock) = CreateSigningUserAction(userProfileToReturn: userProfile, testBpmnfilename: "signing-task-process-missing-config.bpmn"); + var instance = new Instance() + { + Id = "500000/b194e9f5-02d0-41bc-8461-a0cbac8a6efc", + InstanceOwner = new() + { + PartyId = "5000", + }, + Process = new() + { + CurrentTask = new() + { + ElementId = "Task2" + } + }, + Data = new() + { + new() + { + Id = "a499c3ef-e88a-436b-8650-1c43e5037ada", + DataType = "Model" + } + } + }; + var userActionContext = new UserActionContext(instance, 1337); + + // Act + await Assert.ThrowsAsync(async () => await userAction.HandleAction(userActionContext)); + signClientMock.VerifyNoOtherCalls(); + } + + private static (SigningUserAction SigningUserAction, Mock SignClientMock) CreateSigningUserAction(UserProfile userProfileToReturn = null, PlatformHttpException platformHttpExceptionToThrow = null, string testBpmnfilename = "signing-task-process.bpmn") + { + IProcessReader processReader = ProcessTestUtils.SetupProcessReader(testBpmnfilename, Path.Combine("Features", "Action", "TestData")); + + var profileClientMock = new Mock(); + var signingClientMock = new Mock(); + profileClientMock.Setup(p => p.GetUserProfile(It.IsAny())).ReturnsAsync(userProfileToReturn); + if (platformHttpExceptionToThrow != null) + { + signingClientMock.Setup(p => p.SignDataElements(It.IsAny())).ThrowsAsync(platformHttpExceptionToThrow); + } + + return (new SigningUserAction(processReader, new NullLogger(), profileClientMock.Object, signingClientMock.Object), signingClientMock); + } + + private bool AssertSigningContextAsExpected(SignatureContext s1, SignatureContext s2) + { + s1.Should().BeEquivalentTo(s2); + return true; + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/appmetadata.json b/test/Altinn.App.Core.Tests/Features/Action/TestData/appmetadata.json new file mode 100644 index 000000000..51330e670 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/appmetadata.json @@ -0,0 +1,72 @@ +{ + "id": "ttd/vga-dev-v8", + "org": "ttd", + "title": { + "nb": "vga-dev-v8", + "en": "vga-dev-v8" + }, + "dataTypes": [ + { + "id": "ref-data-as-pdf", + "allowedContentTypes": [ + "application/pdf" + ], + "maxCount": 0, + "minCount": 0, + "enablePdfCreation": true + }, + { + "id": "Model", + "allowedContentTypes": [ + "application/xml" + ], + "appLogic": { + "autoCreate": true, + "classRef": "Altinn.App.Models.Model", + "allowAnonymousOnStateless": false, + "autoDeleteOnProcessEnd": false + }, + "taskId": "Task_1", + "maxCount": 1, + "minCount": 1, + "enablePdfCreation": true + }, + { + "id": "Extra", + "allowedContentTypes": [ + "application/xml" + ], + "appLogic": { + "autoCreate": true, + "classRef": "Altinn.App.Models.Extra", + "allowAnonymousOnStateless": false, + "autoDeleteOnProcessEnd": false + }, + "taskId": "Task_3", + "maxCount": 1, + "minCount": 1, + "enablePdfCreation": true + }, + { + "id": "signature", + "allowedContentTypes": [ + "application/json" + ], + "taskId": "Task_2", + "maxCount": 1, + "minCount": 0, + "enablePdfCreation": false + } + ], + "partyTypesAllowed": { + "bankruptcyEstate": false, + "organisation": false, + "person": false, + "subUnit": false + }, + "autoDeleteOnProcessEnd": false, + "created": "2022-10-21T07:30:47.2710111Z", + "createdBy": "tjololo", + "lastChanged": "2022-10-21T07:30:47.2710121Z", + "lastChangedBy": "tjololo" +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-missing-signee-userid.json b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-missing-signee-userid.json new file mode 100644 index 000000000..078e5f77f --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-missing-signee-userid.json @@ -0,0 +1,21 @@ +{ + "id": "061c0677-ea2a-4647-b754-42b08b986194", + "instanceGuid": "a32520b0-db9e-41a5-ac17-517474d9a0eb", + "signedTime": "2023-06-22T08:53:38.7521813Z", + "signeeInfo": { + "personNumber": "01039012345", + "organisationNumber": null + }, + "dataElementSignatures": [ + { + "dataElementId": "c4e56a1e-887e-411a-baaf-3ff9d71e9b52", + "sha256Hash": "b21b56007c4e0b05a94053ec046e20bad5af0949cf4c0761b9b4d6832b4bf22c", + "signed": true + }, + { + "dataElementId": "d076cc95-78b1-412b-942c-60559667b0f0", + "sha256Hash": "b098326ba147e5a17fcaddbf6bdb4c7262abb0978094300f8aaa8219435b669f", + "signed": true + } + ] +} diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-missing-signee.json b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-missing-signee.json new file mode 100644 index 000000000..4882d8ab7 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-missing-signee.json @@ -0,0 +1,17 @@ +{ + "id": "061c0677-ea2a-4647-b754-42b08b986194", + "instanceGuid": "a32520b0-db9e-41a5-ac17-517474d9a0eb", + "signedTime": "2023-06-22T08:53:38.7521813Z", + "dataElementSignatures": [ + { + "dataElementId": "c4e56a1e-887e-411a-baaf-3ff9d71e9b52", + "sha256Hash": "b21b56007c4e0b05a94053ec046e20bad5af0949cf4c0761b9b4d6832b4bf22c", + "signed": true + }, + { + "dataElementId": "d076cc95-78b1-412b-942c-60559667b0f0", + "sha256Hash": "b098326ba147e5a17fcaddbf6bdb4c7262abb0978094300f8aaa8219435b669f", + "signed": true + } + ] +} diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-signee-userid-null.json b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-signee-userid-null.json new file mode 100644 index 000000000..1efcf2a9f --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-signee-userid-null.json @@ -0,0 +1,22 @@ +{ + "id": "061c0677-ea2a-4647-b754-42b08b986194", + "instanceGuid": "a32520b0-db9e-41a5-ac17-517474d9a0eb", + "signedTime": "2023-06-22T08:53:38.7521813Z", + "signeeInfo": { + "userId": null, + "personNumber": "01039012345", + "organisationNumber": null + }, + "dataElementSignatures": [ + { + "dataElementId": "c4e56a1e-887e-411a-baaf-3ff9d71e9b52", + "sha256Hash": "b21b56007c4e0b05a94053ec046e20bad5af0949cf4c0761b9b4d6832b4bf22c", + "signed": true + }, + { + "dataElementId": "d076cc95-78b1-412b-942c-60559667b0f0", + "sha256Hash": "b098326ba147e5a17fcaddbf6bdb4c7262abb0978094300f8aaa8219435b669f", + "signed": true + } + ] +} diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/signature.json b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature.json new file mode 100644 index 000000000..c2ddae8ea --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature.json @@ -0,0 +1,22 @@ +{ + "id": "061c0677-ea2a-4647-b754-42b08b986194", + "instanceGuid": "a32520b0-db9e-41a5-ac17-517474d9a0eb", + "signedTime": "2023-06-22T08:53:38.7521813Z", + "signeeInfo": { + "userId": "1337", + "personNumber": "01039012345", + "organisationNumber": null + }, + "dataElementSignatures": [ + { + "dataElementId": "c4e56a1e-887e-411a-baaf-3ff9d71e9b52", + "sha256Hash": "b21b56007c4e0b05a94053ec046e20bad5af0949cf4c0761b9b4d6832b4bf22c", + "signed": true + }, + { + "dataElementId": "d076cc95-78b1-412b-942c-60559667b0f0", + "sha256Hash": "b098326ba147e5a17fcaddbf6bdb4c7262abb0978094300f8aaa8219435b669f", + "signed": true + } + ] +} diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process-missing-config.bpmn b/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process-missing-config.bpmn new file mode 100644 index 000000000..6daea2280 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process-missing-config.bpmn @@ -0,0 +1,50 @@ + + + + + Flow1 + + + + Flow1 + Flow2 + + + + + + data + + + + + + Flow2 + Flow3 + + + + + + + signing + + + Model + + + + + + + + Flow3 + + + diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process.bpmn b/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process.bpmn new file mode 100644 index 000000000..43afdc77c --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process.bpmn @@ -0,0 +1,51 @@ + + + + + Flow1 + + + + Flow1 + Flow2 + + + + + + data + + + + + + Flow2 + Flow3 + + + + + + + signing + + + Model + + signature + + + + + + + Flow3 + + + diff --git a/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs b/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs new file mode 100644 index 000000000..e421722b8 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs @@ -0,0 +1,373 @@ +#nullable enable +using System.Security.Claims; +using System.Text; +using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Infrastructure.Clients.Storage; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.Base; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using AltinnCore.Authentication.Constants; +using FluentAssertions; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Action; + +public class UniqueSignatureAuthorizerTests : IDisposable +{ + private readonly Mock _processReaderMock; + private readonly Mock _instanceClientMock; + private readonly Mock _dataClientMock; + private readonly Mock _appMetadataMock; + + public UniqueSignatureAuthorizerTests() + { + _processReaderMock = new Mock(); + _instanceClientMock = new Mock(); + _dataClientMock = new Mock(); + _appMetadataMock = new Mock(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_uniqueFromSignaturesInDataTypes_not_defined() + { + ProcessElement processTask = new ProcessTask(); + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask); + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(new ClaimsPrincipal(), new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_uniqueFromSignaturesInDataTypes_null() + { + ProcessElement? processTask = null; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask); + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(new ClaimsPrincipal(), new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_SignatureConfiguration_is_null() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = null + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1000"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_TaskExtension_is_null() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = null + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1000"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_other_user_has_signed_previously() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = new() + { + UniqueFromSignaturesInDataTypes = new() + { + "signature" + } + } + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1000"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + _instanceClientMock.Verify(i => i.GetInstance("xunit-app", "ttd", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"))); + _appMetadataMock.Verify(a => a.GetApplicationMetadata()); + _dataClientMock.Verify(d => d.GetBinaryData("ttd", "xunit-app", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"), Guid.Parse("ca62613c-f058-4899-b962-89dd6496a751"))); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_false_if_same_user_has_signed_previously() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = new() + { + UniqueFromSignaturesInDataTypes = new() + { + "signature" + } + } + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + _instanceClientMock.Verify(i => i.GetInstance("xunit-app", "ttd", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"))); + _appMetadataMock.Verify(a => a.GetApplicationMetadata()); + _dataClientMock.Verify(d => d.GetBinaryData("ttd", "xunit-app", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"), Guid.Parse("ca62613c-f058-4899-b962-89dd6496a751"))); + result.Should().BeFalse(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_taskID_is_null() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = new() + { + UniqueFromSignaturesInDataTypes = new() + { + "signature" + } + } + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), null, "sign")); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_dataelement_not_of_type_SignDocument() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = new() + { + UniqueFromSignaturesInDataTypes = new() + { + "signature" + } + } + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask, "signing-task-process.bpmn"); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + _instanceClientMock.Verify(i => i.GetInstance("xunit-app", "ttd", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"))); + _appMetadataMock.Verify(a => a.GetApplicationMetadata()); + _dataClientMock.Verify(d => d.GetBinaryData("ttd", "xunit-app", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"), Guid.Parse("ca62613c-f058-4899-b962-89dd6496a751"))); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_signdumcument_is_missing_signee() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = new() + { + UniqueFromSignaturesInDataTypes = new() + { + "signature" + } + } + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask, "signature-missing-signee.json"); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + _instanceClientMock.Verify(i => i.GetInstance("xunit-app", "ttd", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"))); + _appMetadataMock.Verify(a => a.GetApplicationMetadata()); + _dataClientMock.Verify(d => d.GetBinaryData("ttd", "xunit-app", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"), Guid.Parse("ca62613c-f058-4899-b962-89dd6496a751"))); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_signdumcument_is_missing_signee_userid() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = new() + { + UniqueFromSignaturesInDataTypes = new() + { + "signature" + } + } + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask, "signature-missing-signee-userid.json"); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + _instanceClientMock.Verify(i => i.GetInstance("xunit-app", "ttd", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"))); + _appMetadataMock.Verify(a => a.GetApplicationMetadata()); + _dataClientMock.Verify(d => d.GetBinaryData("ttd", "xunit-app", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"), Guid.Parse("ca62613c-f058-4899-b962-89dd6496a751"))); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_signdumcument_signee_userid_is_null() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = new() + { + UniqueFromSignaturesInDataTypes = new() + { + "signature" + } + } + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask, "signature-signee-userid-null.json"); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + _instanceClientMock.Verify(i => i.GetInstance("xunit-app", "ttd", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"))); + _appMetadataMock.Verify(a => a.GetApplicationMetadata()); + _dataClientMock.Verify(d => d.GetBinaryData("ttd", "xunit-app", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"), Guid.Parse("ca62613c-f058-4899-b962-89dd6496a751"))); + result.Should().BeTrue(); + } + + private UniqueSignatureAuthorizer CreateUniqueSignatureAuthorizer(ProcessElement? task, string signatureFileToRead = "signature.json") + { + _processReaderMock.Setup(sr => sr.GetFlowElement(It.IsAny())).Returns(task); + _appMetadataMock.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(new ApplicationMetadata("ttd/xunit-app")); + _instanceClientMock.Setup(i => i.GetInstance(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new Instance() + { + Data = new List() + { + new() + { + DataType = "signature", + Id = "ca62613c-f058-4899-b962-89dd6496a751", + } + } + }); + FileStream fileStream = File.OpenRead(Path.Combine("Features", "Action", "TestData", signatureFileToRead)); + _dataClientMock.Setup(d => d.GetBinaryData(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(fileStream); + return new UniqueSignatureAuthorizer(_processReaderMock.Object, _instanceClientMock.Object, _dataClientMock.Object, _appMetadataMock.Object); + } + + public void Dispose() + { + _processReaderMock.VerifyNoOtherCalls(); + _instanceClientMock.VerifyNoOtherCalls(); + _dataClientMock.VerifyNoOtherCalls(); + _appMetadataMock.VerifyNoOtherCalls(); + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Action/UserActionServiceTests.cs b/test/Altinn.App.Core.Tests/Features/Action/UserActionServiceTests.cs new file mode 100644 index 000000000..8859288d4 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/UserActionServiceTests.cs @@ -0,0 +1,75 @@ +#nullable enable +using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Models.UserAction; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Action; + +public class UserActionServiceTests +{ + [Fact] + public void GetActionHandlerOrDefault_should_return_DummyActionHandler_for_id_dummy() + { + var factory = new UserActionService(new List() { new DummyUserAction() }); + + IUserAction? userAction = factory.GetActionHandler("dummy"); + + userAction.Should().NotBeNull(); + userAction.Should().BeOfType(); + userAction!.Id.Should().Be("dummy"); + } + + [Fact] + public void GetActionHandlerOrDefault_should_return_first_DummyActionHandler_for_id_dummy_if_multiple() + { + var factory = new UserActionService(new List() { new DummyUserAction(), new DummyUserAction2() }); + + IUserAction? userAction = factory.GetActionHandler("dummy"); + + userAction.Should().NotBeNull(); + userAction.Should().BeOfType(); + userAction!.Id.Should().Be("dummy"); + } + + [Fact] + public void GetActionHandlerOrDefault_should_return_null_if_id_not_found_and_default_not_set() + { + var factory = new UserActionService(new List() { new DummyUserAction() }); + + IUserAction? userAction = factory.GetActionHandler("nonexisting"); + + userAction.Should().BeNull(); + } + + [Fact] + public void GetActionHandlerOrDefault_should_return_null_if_id_is_null_and_default_not_set() + { + var factory = new UserActionService(new List() { new DummyUserAction() }); + + IUserAction? userAction = factory.GetActionHandler(null); + + userAction.Should().BeNull(); + } + + internal class DummyUserAction : IUserAction + { + public string Id => "dummy"; + + public Task HandleAction(UserActionContext context) + { + return Task.FromResult(UserActionResult.SuccessResult()); + } + } + + internal class DummyUserAction2 : IUserAction + { + public string Id => "dummy"; + + public Task HandleAction(UserActionContext context) + { + return Task.FromResult(UserActionResult.SuccessResult(new List() { ClientAction.NextPage() })); + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Validators/Default/DataAnnotationValidatorTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/Default/DataAnnotationValidatorTests.cs new file mode 100644 index 000000000..68c8b6bb8 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/Default/DataAnnotationValidatorTests.cs @@ -0,0 +1,194 @@ +#nullable enable + +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Validation.Default; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Validators.Default; + +public class DataAnnotationValidatorTests : IClassFixture +{ + private readonly DataAnnotationValidator _validator; + + public DataAnnotationValidatorTests(DataAnnotationsTestFixture fixture) + { + _validator = fixture.App.Services.GetRequiredKeyedService(DataAnnotationsTestFixture.DataType); + } + + private class TestClass + { + [Required] + [JsonPropertyName("requiredProperty")] + public string? RequiredProperty { get; set; } + + [StringLength(5)] + [JsonPropertyName("stringLength")] + public string? StringLengthProperty { get; set; } + + [Range(1, 10)] + [JsonPropertyName("range")] + public int RangeProperty { get; set; } + + [RegularExpression("^[0-9]*$")] + [JsonPropertyName("regularExpression")] + public string? RegularExpressionProperty { get; set; } + + [EmailAddress] + public string? EmailAddressProperty { get; set; } + + public TestClass? NestedProperty { get; set; } + } + + [Fact] + public async Task ValidateFormData() + { + // Arrange + var instance = new Instance(); + var dataElement = new DataElement(); + var data = new object(); + + // Prepare + + // Act + var result = await _validator.ValidateFormData(instance, dataElement, data); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public async Task Validate_ValidFormData_NoErrors() + { + // Arrange + var instance = new Instance(); + var dataElement = new DataElement(); + var data = new TestClass() + { + RangeProperty = 3, + RequiredProperty = "test", + EmailAddressProperty = "test@altinn.no", + RegularExpressionProperty = "12345", + StringLengthProperty = "12345", + NestedProperty = new TestClass() + { + RangeProperty = 3, + RequiredProperty = "test", + EmailAddressProperty = "test@altinn.no", + RegularExpressionProperty = "12345", + StringLengthProperty = "12345", + } + }; + + // Act + var result = await _validator.ValidateFormData(instance, dataElement, data); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public async Task ValidateFormData_RequiredProperty() + { + // Arrange + var instance = new Instance(); + var dataElement = new DataElement(); + var data = new TestClass() + { + NestedProperty = new(), + }; + + // Act + var result = await _validator.ValidateFormData(instance, dataElement, data); + + // Assert + result.Should().NotBeNull(); + result.Should().BeEquivalentTo(JsonSerializer.Deserialize>(""" + [ + { + "severity": 1, + "instanceId": null, + "dataElementId": null, + "field": "range", + "code": "The field RangeProperty must be between 1 and 10.", + "description": "The field RangeProperty must be between 1 and 10.", + "source": "ModelState", + "customTextKey": null + }, + { + "severity": 1, + "instanceId": null, + "dataElementId": null, + "field": "requiredProperty", + "code": "The RequiredProperty field is required.", + "description": "The RequiredProperty field is required.", + "source": "ModelState", + "customTextKey": null + }, + { + "severity": 1, + "instanceId": null, + "dataElementId": null, + "field": "NestedProperty.range", + "code": "The field RangeProperty must be between 1 and 10.", + "description": "The field RangeProperty must be between 1 and 10.", + "source": "ModelState", + "customTextKey": null + }, + { + "severity": 1, + "instanceId": null, + "dataElementId": null, + "field": "NestedProperty.requiredProperty", + "code": "The RequiredProperty field is required.", + "description": "The RequiredProperty field is required.", + "source": "ModelState", + "customTextKey": null + } + ] + """)); + } +} + +/// +/// System.ComponentModel.DataAnnotations does not provide an easy way to run validations recursively in a unit test, +/// so we need to instantiate a WebApplication to get the IObjectModelValidator. +/// +/// A full WebApplicationFactory seemed a little overkill, so we just use a WebApplicationBuilder. +/// +public class DataAnnotationsTestFixture : IAsyncDisposable +{ + public const string DataType = "test"; + + private readonly DefaultHttpContext _httpContext = new DefaultHttpContext(); + + private readonly Mock _httpContextAccessor = new Mock(); + + public WebApplication App { get; } + + public DataAnnotationsTestFixture() + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.Services.AddMvc(); + builder.Services.AddKeyedTransient(DataType); + _httpContextAccessor.Setup(a => a.HttpContext).Returns(_httpContext); + builder.Services.AddSingleton(_httpContextAccessor.Object); + builder.Services.Configure(builder.Configuration.GetSection("GeneralSettings")); + App = builder.Build(); + } + + public ValueTask DisposeAsync() + { + return App.DisposeAsync(); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Validators/Default/DefaultTaskValidatorTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/Default/DefaultTaskValidatorTests.cs new file mode 100644 index 000000000..e3aef2782 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/Default/DefaultTaskValidatorTests.cs @@ -0,0 +1,137 @@ +using Altinn.App.Core.Features.Validation.Default; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Validators.Default; + +public class DefaultTaskValidatorTests +{ + private const string AppId = "tdd/test"; + private const string UnlimitedTaskId = "UnlimitedTask"; + private const string OneRequiredElementTaskId = "OneRequiredElement"; + private const string UnlimitedDataType = "UnlimitedDataId"; + private const string OneRequiredDataType = "OneRequiredDataId"; + + private readonly ApplicationMetadata _applicationMetadata = new(AppId) + { + DataTypes = new List() + { + new() + { + Id = UnlimitedDataType, + TaskId = UnlimitedTaskId, + MaxCount = 0, + MinCount = 0, + }, + new() + { + Id = OneRequiredDataType, + TaskId = OneRequiredElementTaskId, + MinCount = 1, + MaxCount = 1, + } + } + }; + + private readonly Instance _instance = new Instance() + { + Id = $"1234/{Guid.NewGuid()}", + AppId = AppId, + Data = new List(), + }; + + private readonly Mock _appMetadataMock = new(); + private readonly DefaultTaskValidator _sut; + + public DefaultTaskValidatorTests() + { + _appMetadataMock + .Setup(a => a.GetApplicationMetadata()) + .ReturnsAsync(_applicationMetadata); + _sut = new DefaultTaskValidator(_appMetadataMock.Object); + } + + [Fact] + public async Task UnknownTask_NoData_ReturnsNoErrors() + { + var issues = await _sut.ValidateTask(_instance, "unknownTask"); + issues.Should().BeEmpty(); + } + + [Fact] + public async Task UnknownTask_UnknownData_ReturnsNoErrors() + { + _instance.Data.Add(new DataElement + { + DataType = "unknownDataType" + }); + var issues = await _sut.ValidateTask(_instance, "unknownTask"); + issues.Should().BeEmpty(); + } + + [Fact] + public async Task UnlimitedTask_NoData_ReturnsNoErrors() + { + var issues = await _sut.ValidateTask(_instance, UnlimitedTaskId); + issues.Should().BeEmpty(); + } + + [Fact] + public async Task UnlimitedTask_100Data_ReturnsNoErrors() + { + for (var i = 0; i < 100; i++) + { + _instance.Data.Add(new DataElement + { + DataType = UnlimitedDataType + }); + } + + var issues = await _sut.ValidateTask(_instance, UnlimitedTaskId); + issues.Should().BeEmpty(); + } + + [Fact] + public async Task OneRequired_TheOneRequired_ReturnsNoErrors() + { + _instance.Data.Add(new() + { + DataType = OneRequiredDataType + }); + var issues = await _sut.ValidateTask(_instance, OneRequiredElementTaskId); + issues.Should().BeEmpty(); + } + + [Fact] + public async Task OneRequired_NoData_ReturnsError() + { + var issues = await _sut.ValidateTask(_instance, OneRequiredElementTaskId); + var issue = issues.Should().ContainSingle().Which; + issue.Code.Should().Be("TooFewDataElementsOfType"); + issue.Severity.Should().Be(ValidationIssueSeverity.Error); + issue.Field.Should().Be(OneRequiredDataType); + } + + [Fact] + public async Task OneRequired_2Data_ReturnsError() + { + _instance.Data.Add(new() + { + DataType = OneRequiredDataType + }); + _instance.Data.Add(new() + { + DataType = OneRequiredDataType + }); + var issues = await _sut.ValidateTask(_instance, OneRequiredElementTaskId); + var issue = issues.Should().ContainSingle().Which; + issue.Code.Should().Be("TooManyDataElementsOfType"); + issue.Severity.Should().Be(ValidationIssueSeverity.Error); + issue.Field.Should().Be(OneRequiredDataType); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Validators/Default/ExpressionValidatorTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/Default/ExpressionValidatorTests.cs new file mode 100644 index 000000000..c4668732f --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/Default/ExpressionValidatorTests.cs @@ -0,0 +1,125 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Features.Validation.Default; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Models.Layout; +using Altinn.App.Core.Models.Validation; +using Altinn.App.Core.Tests.Helpers; +using Altinn.App.Core.Tests.LayoutExpressions; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; +using Xunit.Sdk; + +namespace Altinn.App.Core.Tests.Features.Validators.Default; + +public class ExpressionValidatorTests +{ + private readonly ExpressionValidator _validator; + private readonly Mock> _logger = new(); + private readonly Mock _appResources = new(MockBehavior.Strict); + private readonly IOptions _frontendSettings = Options.Create(new FrontEndSettings()); + private readonly Mock _layoutInitializer; + + public ExpressionValidatorTests() + { + _layoutInitializer = new(MockBehavior.Strict, _appResources.Object, _frontendSettings) { CallBase = false }; + _validator = + new ExpressionValidator(_logger.Object, _appResources.Object, _layoutInitializer.Object); + } + + [Theory] + [ExpressionTest] + public async Task RunExpressionValidationTest(ExpressionValidationTestModel testCase) + { + var instance = new Instance(); + var dataElement = new DataElement(); + + var dataModel = new JsonDataModel(testCase.FormData); + + var evaluatorState = new LayoutEvaluatorState(dataModel, testCase.Layouts, _frontendSettings.Value, instance); + _layoutInitializer + .Setup(init => init.Init(It.Is(i => i == instance), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(evaluatorState); + _appResources + .Setup(ar => ar.GetValidationConfiguration(null)) + .Returns(JsonSerializer.Serialize(testCase.ValidationConfig)); + + LayoutEvaluator.RemoveHiddenData(evaluatorState, RowRemovalOption.SetToNull); + var validationIssues = await _validator.ValidateFormData(instance, dataElement, null!); + + var result = validationIssues.Select(i => new + { + Message = i.CustomTextKey, + Severity = i.Severity, + Field = i.Field, + }); + + var expected = testCase.Expects.Select(e => new + { + Message = e.Message, + Severity = e.Severity, + Field = e.Field, + }); + + result.Should().BeEquivalentTo(expected); + } +} + +public class ExpressionTestAttribute : DataAttribute +{ + private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions + { + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + public override IEnumerable GetData(MethodInfo methodInfo) + { + var files = Directory.GetFiles(Path.Join("Features", "Validators", "shared-expression-validation-tests")); + + foreach (var file in files) + { + var data = File.ReadAllText(file); + ExpressionValidationTestModel testCase = JsonSerializer.Deserialize( + data, + JsonSerializerOptions)!; + yield return new object[] { testCase }; + } + } +} + +public class ExpressionValidationTestModel +{ + public string Name { get; set; } + + public ExpectedObject[] Expects { get; set; } + + public JsonElement ValidationConfig { get; set; } + + public JsonObject FormData { get; set; } + + [JsonConverter(typeof(LayoutModelConverterFromObject))] + public LayoutModel Layouts { get; set; } + + public class ExpectedObject + { + public string Message { get; set; } + + [JsonConverter(typeof(FrontendSeverityConverter))] + public ValidationIssueSeverity Severity { get; set; } + + public string Field { get; set; } + + public string ComponentId { get; set; } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/Default/LegacyIValidationFormDataTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/Default/LegacyIValidationFormDataTests.cs new file mode 100644 index 000000000..c0388866e --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/Default/LegacyIValidationFormDataTests.cs @@ -0,0 +1,138 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Validation.Default; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Validators.Default +{ + public class LegacyIValidationFormDataTests + { + private readonly LegacyIInstanceValidatorFormDataValidator _validator; + private readonly Mock _instanceValidator = new(); + + public LegacyIValidationFormDataTests() + { + var generalSettings = new GeneralSettings(); + _validator = + new LegacyIInstanceValidatorFormDataValidator(_instanceValidator.Object, Options.Create(generalSettings)); + } + + [Fact] + public async Task ValidateFormData_NoErrors() + { + // Arrange + var data = new object(); + + var validator = new LegacyIInstanceValidatorFormDataValidator(null, Options.Create(new GeneralSettings())); + validator.HasRelevantChanges(data, data).Should().BeFalse(); + + // Act + var result = await validator.ValidateFormData(new Instance(), new DataElement(), data); + + // Assert + Assert.Empty(result); + } + + [Fact] + public async Task ValidateFormData_WithErrors() + { + // Arrange + var data = new object(); + + _instanceValidator + .Setup(iv => iv.ValidateData(It.IsAny(), It.IsAny())) + .Callback((object _, ModelStateDictionary modelState) => + { + modelState.AddModelError("test", "test"); + modelState.AddModelError("ddd", "*FIXED*test"); + }); + + // Act + var result = await _validator.ValidateFormData(new Instance(), new DataElement(), data); + + // Assert + result.Should().BeEquivalentTo( + JsonSerializer.Deserialize>(""" + [ + { + "severity": 4, + "instanceId": null, + "dataElementId": null, + "field": "ddd", + "code": "test", + "description": "test", + "source": "Custom", + "customTextKey": null + }, + { + "severity": 1, + "instanceId": null, + "dataElementId": null, + "field": "test", + "code": "test", + "description": "test", + "source": "Custom", + "customTextKey": null + } + ] + """)); + } + + private class TestModel + { + [JsonPropertyName("test")] + public string Test { get; set; } + + public int IntegerWithout { get; set; } + + [JsonPropertyName("child")] + public TestModel Child { get; set; } + + [JsonPropertyName("children")] + public List TestList { get; set; } + } + + [Theory] + [InlineData("test", "test", "test with small case")] + [InlineData("Test", "test", "test with capital case gets rewritten")] + [InlineData("NotModelMatch", "NotModelMatch", "Error that does not mach model is kept as is")] + [InlineData("Child.TestList[2].child", "child.children[2].child", "TestList is renamed to children because of JsonPropertyName")] + [InlineData("test.children.child", "test.children.child", "valid JsonPropertyName based path is kept as is")] + public async Task ValidateErrorAndMappingWithCustomModel(string errorKey, string field, string errorMessage) + { + // Arrange + var data = new TestModel(); + + _instanceValidator + .Setup(iv => iv.ValidateData(It.IsAny(), It.IsAny())) + .Callback((object _, ModelStateDictionary modelState) => + { + modelState.AddModelError(errorKey, errorMessage); + modelState.AddModelError(errorKey, "*FIXED*" + errorMessage + " Fixed"); + }); + + // Act + var result = await _validator.ValidateFormData(new Instance(), new DataElement(), data); + + // Assert + result.Should().HaveCount(2); + var errorIssue = result.Should().ContainSingle(i => i.Severity == ValidationIssueSeverity.Error).Which; + errorIssue.Field.Should().Be(field); + errorIssue.Severity.Should().Be(ValidationIssueSeverity.Error); + errorIssue.Description.Should().Be(errorMessage); + + var fixedIssue = result.Should().ContainSingle(i => i.Severity == ValidationIssueSeverity.Fixed).Which; + fixedIssue.Field.Should().Be(field); + fixedIssue.Severity.Should().Be(ValidationIssueSeverity.Fixed); + fixedIssue.Description.Should().Be(errorMessage + " Fixed"); + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Validators/ExpressionValidationTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/ExpressionValidationTests.cs new file mode 100644 index 000000000..a8bd5f92c --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/ExpressionValidationTests.cs @@ -0,0 +1,99 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Features.Validation.Default; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Models.Layout; +using Altinn.App.Core.Models.Validation; +using Altinn.App.Core.Tests.Helpers; +using Altinn.App.Core.Tests.LayoutExpressions; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; +using Xunit.Sdk; + +namespace Altinn.App.Core.Tests.Features.Validators; + +public class ExpressionValidationTests +{ + [Theory] + [ExpressionTest] + public void RunExpressionValidationTest(ExpressionValidationTestModel testCase) + { + var logger = Mock.Of>(); + var dataModel = new JsonDataModel(testCase.FormData); + var evaluatorState = new LayoutEvaluatorState(dataModel, testCase.Layouts, new(), new()); + + LayoutEvaluator.RemoveHiddenData(evaluatorState, RowRemovalOption.SetToNull); + var validationIssues = ExpressionValidator.Validate(testCase.ValidationConfig, evaluatorState, logger).ToArray(); + + var result = validationIssues.Select(i => new + { + Message = i.CustomTextKey, + Severity = i.Severity, + Field = i.Field, + }); + + var expected = testCase.Expects.Select(e => new + { + Message = e.Message, + Severity = e.Severity, + Field = e.Field, + }); + + result.Should().BeEquivalentTo(expected); + } +} + +public class ExpressionTestAttribute : DataAttribute +{ + public override IEnumerable GetData(MethodInfo methodInfo) + { + return Directory + .GetFiles(Path.Join("Features", "Validators", "shared-expression-validation-tests")) + .Select(file => + { + var data = File.ReadAllText(file); + return new object[] + { + JsonSerializer.Deserialize( + data, + new JsonSerializerOptions + { + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + })! + }; + }); + } +} + +public class ExpressionValidationTestModel +{ + public string Name { get; set; } + + public ExpectedObject[] Expects { get; set; } + + public JsonElement ValidationConfig { get; set; } + + public JsonObject FormData { get; set; } + + [JsonConverter(typeof(LayoutModelConverterFromObject))] + public LayoutModel Layouts { get; set; } + + public class ExpectedObject + { + public string Message { get; set; } + + [JsonConverter(typeof(FrontendSeverityConverter))] + public ValidationIssueSeverity Severity { get; set; } + + public string Field { get; set; } + + public string ComponentId { get; set; } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/GenericValidatorTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/GenericValidatorTests.cs new file mode 100644 index 000000000..687534bb6 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/GenericValidatorTests.cs @@ -0,0 +1,74 @@ +#nullable enable +using System.Text.Json.Serialization; +using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Validators; + +public class GenericValidatorTests +{ + private class MyModel + { + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("age")] + public int? Age { get; set; } + + [JsonPropertyName("children")] + public List? Children { get; set; } + } + + private class TestValidator : GenericFormDataValidator + { + public TestValidator() : base("MyType") + { + } + + protected override bool HasRelevantChanges(MyModel current, MyModel previous) + { + throw new NotImplementedException(); + } + + protected override Task ValidateFormData(Instance instance, DataElement dataElement, MyModel data) + { + AddValidationIssue(new ValidationIssue() + { + Severity = ValidationIssueSeverity.Informational, + Description = "Test info", + }); + + CreateValidationIssue(c => c.Name, "Test warning", severity: ValidationIssueSeverity.Warning); + var childIndex = 4; + CreateValidationIssue(c => c.Children![childIndex].Children![0].Name, "childrenError", severity: ValidationIssueSeverity.Error); + + return Task.CompletedTask; + } + } + + [Fact] + public async Task VerifyTestValidator() + { + var testValidator = new TestValidator(); + var instance = new Instance(); + var dataElement = new DataElement(); + var data = new MyModel(); + + var validationIssues = await testValidator.ValidateFormData(instance, dataElement, data); + validationIssues.Should().HaveCount(3); + + var info = validationIssues.Should().ContainSingle(c => c.Severity == ValidationIssueSeverity.Informational).Which; + info.Description.Should().Be("Test info"); + + var warning = validationIssues.Should().ContainSingle(c => c.Severity == ValidationIssueSeverity.Warning).Which; + warning.Description.Should().Be("Test warning"); + warning.Field.Should().Be("name"); + + var error = validationIssues.Should().ContainSingle(c => c.Severity == ValidationIssueSeverity.Error).Which; + error.Description.Should().Be("childrenError"); + error.Field.Should().Be("children[4].children[0].name"); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Validators/ValidationAppSITests.cs b/test/Altinn.App.Core.Tests/Features/Validators/ValidationAppSITests.cs deleted file mode 100644 index 207967acd..000000000 --- a/test/Altinn.App.Core.Tests/Features/Validators/ValidationAppSITests.cs +++ /dev/null @@ -1,259 +0,0 @@ -#nullable enable -using System.Text.Json.Serialization; -using Altinn.App.Core.Features; -using Altinn.App.Core.Features.Validation; -using Altinn.App.Core.Interface; -using Altinn.App.Core.Internal.App; -using Altinn.App.Core.Internal.AppModel; -using Altinn.App.Core.Internal.Expressions; -using Altinn.App.Core.Models.Validation; -using Altinn.Platform.Storage.Interface.Enums; -using Altinn.Platform.Storage.Interface.Models; -using FluentAssertions; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Moq; -using Xunit; - -namespace Altinn.App.Core.Tests.Features.Validators; - -public class ValidationAppSITests -{ - [Fact] - public async Task FileScanEnabled_VirusFound_ValidationShouldFail() - { - ValidationAppSI validationAppSI = ConfigureMockServicesForValidation(); - - var instance = new Instance(); - var dataType = new DataType() { EnableFileScan = true }; - var dataElement = new DataElement() - { - FileScanResult = FileScanResult.Infected - }; - - List validationIssues = await validationAppSI.ValidateDataElement(instance, dataType, dataElement); - - validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileInfected").Should().NotBeNull(); - } - - [Fact] - public async Task FileScanEnabled_PendingScanNotEnabled_ValidationShouldNotFail() - { - ValidationAppSI validationAppSI = ConfigureMockServicesForValidation(); - - var instance = new Instance(); - var dataType = new DataType() { EnableFileScan = true }; - var dataElement = new DataElement() - { - FileScanResult = FileScanResult.Pending - }; - - List validationIssues = await validationAppSI.ValidateDataElement(instance, dataType, dataElement); - - validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileScanPending").Should().BeNull(); - } - - [Fact] - public async Task FileScanEnabled_PendingScanEnabled_ValidationShouldNotFail() - { - ValidationAppSI validationAppSI = ConfigureMockServicesForValidation(); - - var instance = new Instance(); - var dataType = new DataType() { EnableFileScan = true, ValidationErrorOnPendingFileScan = true }; - var dataElement = new DataElement() - { - FileScanResult = FileScanResult.Pending - }; - - List validationIssues = await validationAppSI.ValidateDataElement(instance, dataType, dataElement); - - validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileScanPending").Should().NotBeNull(); - } - - [Fact] - public async Task FileScanEnabled_Clean_ValidationShouldNotFail() - { - ValidationAppSI validationAppSI = ConfigureMockServicesForValidation(); - - var instance = new Instance(); - var dataType = new DataType() { EnableFileScan = true, ValidationErrorOnPendingFileScan = true }; - var dataElement = new DataElement() - { - FileScanResult = FileScanResult.Clean - }; - - List validationIssues = await validationAppSI.ValidateDataElement(instance, dataType, dataElement); - - validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileInfected").Should().BeNull(); - validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileScanPending").Should().BeNull(); - } - - private static ValidationAppSI ConfigureMockServicesForValidation() - { - Mock> loggerMock = new(); - var dataMock = new Mock(); - var instanceMock = new Mock(); - var instanceValidator = new Mock(); - var appModelMock = new Mock(); - var appResourcesMock = new Mock(); - var appMetadataMock = new Mock(); - var objectModelValidatorMock = new Mock(); - var layoutEvaluatorStateInitializer = new LayoutEvaluatorStateInitializer(appResourcesMock.Object, Microsoft.Extensions.Options.Options.Create(new Configuration.FrontEndSettings())); - var httpContextAccessorMock = new Mock(); - var generalSettings = Microsoft.Extensions.Options.Options.Create(new Configuration.GeneralSettings()); - var appSettings = Microsoft.Extensions.Options.Options.Create(new Configuration.AppSettings()); - - var validationAppSI = new ValidationAppSI( - loggerMock.Object, - dataMock.Object, - instanceMock.Object, - instanceValidator.Object, - appModelMock.Object, - appResourcesMock.Object, - appMetadataMock.Object, - objectModelValidatorMock.Object, - layoutEvaluatorStateInitializer, - httpContextAccessorMock.Object, - generalSettings, - appSettings); - return validationAppSI; - } - - [Fact] - public void ModelKeyToField_NullInputWithoutType_ReturnsNull() - { - ValidationAppSI.ModelKeyToField(null, null!).Should().BeNull(); - } - - [Fact] - public void ModelKeyToField_StringInputWithoutType_ReturnsSameString() - { - ValidationAppSI.ModelKeyToField("null", null!).Should().Be("null"); - } - - [Fact] - public void ModelKeyToField_NullInput_ReturnsNull() - { - ValidationAppSI.ModelKeyToField(null, typeof(TestModel)).Should().BeNull(); - } - - [Fact] - public void ModelKeyToField_StringInput_ReturnsSameString() - { - ValidationAppSI.ModelKeyToField("null", typeof(TestModel)).Should().Be("null"); - } - - [Fact] - public void ModelKeyToField_StringInputWithAttr_ReturnsMappedString() - { - ValidationAppSI.ModelKeyToField("FirstLevelProp", typeof(TestModel)).Should().Be("level1"); - } - - [Fact] - public void ModelKeyToField_SubModel_ReturnsMappedString() - { - ValidationAppSI.ModelKeyToField("SubTestModel.DecimalNumber", typeof(TestModel)).Should().Be("sub.decimal"); - } - - [Fact] - public void ModelKeyToField_SubModelNullable_ReturnsMappedString() - { - ValidationAppSI.ModelKeyToField("SubTestModel.StringNullable", typeof(TestModel)).Should().Be("sub.nullableString"); - } - - [Fact] - public void ModelKeyToField_SubModelWithSubmodel_ReturnsMappedString() - { - ValidationAppSI.ModelKeyToField("SubTestModel.StringNullable", typeof(TestModel)).Should().Be("sub.nullableString"); - } - - [Fact] - public void ModelKeyToField_SubModelNull_ReturnsMappedString() - { - ValidationAppSI.ModelKeyToField("SubTestModelNull.DecimalNumber", typeof(TestModel)).Should().Be("subnull.decimal"); - } - - [Fact] - public void ModelKeyToField_SubModelNullNullable_ReturnsMappedString() - { - ValidationAppSI.ModelKeyToField("SubTestModelNull.StringNullable", typeof(TestModel)).Should().Be("subnull.nullableString"); - } - - [Fact] - public void ModelKeyToField_SubModelNullWithSubmodel_ReturnsMappedString() - { - ValidationAppSI.ModelKeyToField("SubTestModelNull.StringNullable", typeof(TestModel)).Should().Be("subnull.nullableString"); - } - - // Test lists - [Fact] - public void ModelKeyToField_List_IgnoresMissingIndex() - { - ValidationAppSI.ModelKeyToField("SubTestModelList.StringNullable", typeof(TestModel)).Should().Be("subList.nullableString"); - } - - [Fact] - public void ModelKeyToField_List_ProxiesIndex() - { - ValidationAppSI.ModelKeyToField("SubTestModelList[123].StringNullable", typeof(TestModel)).Should().Be("subList[123].nullableString"); - } - - [Fact] - public void ModelKeyToField_ListOfList_ProxiesIndex() - { - ValidationAppSI.ModelKeyToField("SubTestModelList[123].ListOfDecimal[5]", typeof(TestModel)).Should().Be("subList[123].decimalList[5]"); - } - - [Fact] - public void ModelKeyToField_ListOfList_IgnoresMissing() - { - ValidationAppSI.ModelKeyToField("SubTestModelList[123].ListOfDecimal", typeof(TestModel)).Should().Be("subList[123].decimalList"); - } - - [Fact] - public void ModelKeyToField_ListOfListNullable_IgnoresMissing() - { - ValidationAppSI.ModelKeyToField("SubTestModelList[123].ListOfNullableDecimal", typeof(TestModel)).Should().Be("subList[123].nullableDecimalList"); - } - - [Fact] - public void ModelKeyToField_ListOfListOfListNullable_IgnoresMissingButPropagatesOthers() - { - ValidationAppSI.ModelKeyToField("SubTestModelList[123].SubTestModelList.ListOfNullableDecimal[123456]", typeof(TestModel)).Should().Be("subList[123].subList.nullableDecimalList[123456]"); - } - - public class TestModel - { - [JsonPropertyName("level1")] - public string FirstLevelProp { get; set; } = default!; - - [JsonPropertyName("sub")] - public SubTestModel SubTestModel { get; set; } = default!; - - [JsonPropertyName("subnull")] - public SubTestModel? SubTestModelNull { get; set; } = default!; - - [JsonPropertyName("subList")] - public List SubTestModelList { get; set; } = default!; - } - - public class SubTestModel - { - [JsonPropertyName("decimal")] - public decimal DecimalNumber { get; set; } = default!; - - [JsonPropertyName("nullableString")] - public string? StringNullable { get; set; } = default!; - - [JsonPropertyName("decimalList")] - public List ListOfDecimal { get; set; } = default!; - - [JsonPropertyName("nullableDecimalList")] - public List ListOfNullableDecimal { get; set; } = default!; - - [JsonPropertyName("subList")] - public List SubTestModelList { get; set; } = default!; - } -} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceOldTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceOldTests.cs new file mode 100644 index 000000000..9dd8aaada --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceOldTests.cs @@ -0,0 +1,384 @@ +#nullable enable +using System.Text.Json.Serialization; +using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Features.Validation.Default; +using Altinn.App.Core.Features.Validation.Helpers; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Enums; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Validators; + +public class ValidationServiceOldTests +{ + private readonly Mock> _loggerMock = new(); + private readonly Mock _dataClientMock = new(); + private readonly Mock _appModelMock = new(); + private readonly Mock _appMetadataMock = new(); + private readonly ServiceCollection _serviceCollection = new(); + + private readonly ApplicationMetadata _applicationMetadata = new("tdd/test") + { + DataTypes = new List() + { + new DataType() + { + Id = "test", + TaskId = "Task_1", + EnableFileScan = false, + ValidationErrorOnPendingFileScan = false, + } + } + }; + + public ValidationServiceOldTests() + { + _serviceCollection.AddSingleton(_loggerMock.Object); + _serviceCollection.AddSingleton(_dataClientMock.Object); + _serviceCollection.AddSingleton(); + _serviceCollection.AddSingleton(_appModelMock.Object); + _serviceCollection.AddSingleton(_appMetadataMock.Object); + _serviceCollection.AddSingleton(); + _serviceCollection.AddSingleton(); + _appMetadataMock.Setup(am => am.GetApplicationMetadata()).ReturnsAsync(_applicationMetadata); + } + + [Fact] + public async Task FileScanEnabled_VirusFound_ValidationShouldFail() + { + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + IValidationService validationService = serviceProvider.GetRequiredService(); + + var instance = new Instance(); + var dataType = new DataType() { EnableFileScan = true }; + var dataElement = new DataElement() + { + DataType = "test", + FileScanResult = FileScanResult.Infected + }; + + List validationIssues = await validationService.ValidateDataElement(instance, dataElement, dataType); + + validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileInfected").Should().NotBeNull(); + } + + [Fact] + public async Task FileScanEnabled_PendingScanNotEnabled_ValidationShouldNotFail() + { + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + IValidationService validationService = serviceProvider.GetRequiredService(); + + var dataType = new DataType() + { Id = "test", TaskId = "Task_1", AppLogic = null, EnableFileScan = true }; + var instance = new Instance() + { + }; + var dataElement = new DataElement() + { + DataType = "test", + FileScanResult = FileScanResult.Pending, + }; + + List validationIssues = await validationService.ValidateDataElement(instance, dataElement, dataType); + + validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileScanPending").Should().BeNull(); + } + + [Fact] + public async Task FileScanEnabled_PendingScanEnabled_ValidationShouldNotFail() + { + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + IValidationService validationService = serviceProvider.GetRequiredService(); + + var instance = new Instance(); + var dataType = new DataType() { EnableFileScan = true, ValidationErrorOnPendingFileScan = true }; + var dataElement = new DataElement() + { + DataType = "test", + FileScanResult = FileScanResult.Pending + }; + + List validationIssues = await validationService.ValidateDataElement(instance, dataElement, dataType); + + validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileScanPending").Should().NotBeNull(); + } + + [Fact] + public async Task FileScanEnabled_Clean_ValidationShouldNotFail() + { + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + IValidationService validationService = serviceProvider.GetRequiredService(); + + var instance = new Instance(); + var dataType = new DataType() { EnableFileScan = true, ValidationErrorOnPendingFileScan = true }; + var dataElement = new DataElement() + { + DataType = "test", + FileScanResult = FileScanResult.Clean, + }; + + List validationIssues = await validationService.ValidateDataElement(instance, dataElement, dataType); + + validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileInfected").Should().BeNull(); + validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileScanPending").Should().BeNull(); + } + + [Fact] + public async Task ValidateAndUpdateProcess_set_canComplete_validationstatus_and_return_empty_list() + { + const string taskId = "Task_1"; + + // Mock setup + var appMetadata = new ApplicationMetadata("ttd/test-app") + { + DataTypes = new List + { + new DataType + { + Id = "data", + TaskId = taskId, + MaxCount = 0, + } + } + }; + _appMetadataMock.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(appMetadata); + + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + IValidationService validationService = serviceProvider.GetRequiredService(); + + // Testdata + var instance = new Instance + { + Data = new List() + { + new() + { + DataType = "data", + ContentType = "application/json" + }, + }, + Process = new ProcessState + { + CurrentTask = new ProcessElementInfo + { + Name = "Task_1" + } + } + }; + + var issues = await validationService.ValidateInstanceAtTask(instance, taskId); + issues.Should().BeEmpty(); + + // instance.Process?.CurrentTask?.Validated.CanCompleteTask.Should().BeTrue(); + // instance.Process?.CurrentTask?.Validated.Timestamp.Should().NotBeNull(); + } + + [Fact] + public async Task ValidateAndUpdateProcess_set_canComplete_false_validationstatus_and_return_list_of_issues() + { + const string taskId = "Task_1"; + + // Mock setup + var appMetadata = new ApplicationMetadata("ttd/test-app") + { + DataTypes = new List + { + new DataType + { + Id = "data", + TaskId = taskId, + MaxCount = 1, + } + } + }; + _appMetadataMock.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(appMetadata); + + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + IValidationService validationService = serviceProvider.GetRequiredService(); + + // Testdata + var instance = new Instance + { + Data = new List() + { + new() + { + Id = "3C8B52A9-9602-4B2E-A217-B4E816ED8DEB", + DataType = "data", + ContentType = "application/json" + }, + new() + { + Id = "3C8B52A9-9602-4B2E-A217-B4E816ED8DEC", + DataType = "data", + ContentType = "application/json" + }, + }, + Process = new ProcessState + { + CurrentTask = new ProcessElementInfo + { + Name = "Task_1" + } + } + }; + + var issues = await validationService.ValidateInstanceAtTask(instance, taskId); + issues.Should().HaveCount(1); + issues.Should().ContainSingle(i => i.Code == ValidationIssueCodes.InstanceCodes.TooManyDataElementsOfType); + + // instance.Process?.CurrentTask?.Validated.CanCompleteTask.Should().BeFalse(); + // instance.Process?.CurrentTask?.Validated.Timestamp.Should().NotBeNull(); + } + + [Fact] + public void ModelKeyToField_NullInputWithoutType_ReturnsNull() + { + ModelStateHelpers.ModelKeyToField(null, null!).Should().BeNull(); + } + + [Fact] + public void ModelKeyToField_StringInputWithoutType_ReturnsSameString() + { + ModelStateHelpers.ModelKeyToField("null", null!).Should().Be("null"); + } + + [Fact] + public void ModelKeyToField_NullInput_ReturnsNull() + { + ModelStateHelpers.ModelKeyToField(null, typeof(TestModel)).Should().BeNull(); + } + + [Fact] + public void ModelKeyToField_StringInput_ReturnsSameString() + { + ModelStateHelpers.ModelKeyToField("null", typeof(TestModel)).Should().Be("null"); + } + + [Fact] + public void ModelKeyToField_StringInputWithAttr_ReturnsMappedString() + { + ModelStateHelpers.ModelKeyToField("FirstLevelProp", typeof(TestModel)).Should().Be("level1"); + } + + [Fact] + public void ModelKeyToField_SubModel_ReturnsMappedString() + { + ModelStateHelpers.ModelKeyToField("SubTestModel.DecimalNumber", typeof(TestModel)).Should().Be("sub.decimal"); + } + + [Fact] + public void ModelKeyToField_SubModelNullable_ReturnsMappedString() + { + ModelStateHelpers.ModelKeyToField("SubTestModel.StringNullable", typeof(TestModel)).Should().Be("sub.nullableString"); + } + + [Fact] + public void ModelKeyToField_SubModelWithSubmodel_ReturnsMappedString() + { + ModelStateHelpers.ModelKeyToField("SubTestModel.StringNullable", typeof(TestModel)).Should().Be("sub.nullableString"); + } + + [Fact] + public void ModelKeyToField_SubModelNull_ReturnsMappedString() + { + ModelStateHelpers.ModelKeyToField("SubTestModelNull.DecimalNumber", typeof(TestModel)).Should().Be("subnull.decimal"); + } + + [Fact] + public void ModelKeyToField_SubModelNullNullable_ReturnsMappedString() + { + ModelStateHelpers.ModelKeyToField("SubTestModelNull.StringNullable", typeof(TestModel)).Should().Be("subnull.nullableString"); + } + + [Fact] + public void ModelKeyToField_SubModelNullWithSubmodel_ReturnsMappedString() + { + ModelStateHelpers.ModelKeyToField("SubTestModelNull.StringNullable", typeof(TestModel)).Should().Be("subnull.nullableString"); + } + + // Test lists + [Fact] + public void ModelKeyToField_List_IgnoresMissingIndex() + { + ModelStateHelpers.ModelKeyToField("SubTestModelList.StringNullable", typeof(TestModel)).Should().Be("subList.nullableString"); + } + + [Fact] + public void ModelKeyToField_List_ProxiesIndex() + { + ModelStateHelpers.ModelKeyToField("SubTestModelList[123].StringNullable", typeof(TestModel)).Should().Be("subList[123].nullableString"); + } + + [Fact] + public void ModelKeyToField_ListOfList_ProxiesIndex() + { + ModelStateHelpers.ModelKeyToField("SubTestModelList[123].ListOfDecimal[5]", typeof(TestModel)).Should().Be("subList[123].decimalList[5]"); + } + + [Fact] + public void ModelKeyToField_ListOfList_IgnoresMissing() + { + ModelStateHelpers.ModelKeyToField("SubTestModelList[123].ListOfDecimal", typeof(TestModel)).Should().Be("subList[123].decimalList"); + } + + [Fact] + public void ModelKeyToField_ListOfListNullable_IgnoresMissing() + { + ModelStateHelpers.ModelKeyToField("SubTestModelList[123].ListOfNullableDecimal", typeof(TestModel)).Should().Be("subList[123].nullableDecimalList"); + } + + [Fact] + public void ModelKeyToField_ListOfListOfListNullable_IgnoresMissingButPropagatesOthers() + { + ModelStateHelpers.ModelKeyToField("SubTestModelList[123].SubTestModelList.ListOfNullableDecimal[123456]", typeof(TestModel)).Should().Be("subList[123].subList.nullableDecimalList[123456]"); + } + + public class TestModel + { + [JsonPropertyName("level1")] + public string FirstLevelProp { get; set; } = default!; + + [JsonPropertyName("sub")] + public SubTestModel SubTestModel { get; set; } = default!; + + [JsonPropertyName("subnull")] + public SubTestModel? SubTestModelNull { get; set; } = default!; + + [JsonPropertyName("subList")] + public List SubTestModelList { get; set; } = default!; + } + + public class SubTestModel + { + [JsonPropertyName("decimal")] + public decimal DecimalNumber { get; set; } = default!; + + [JsonPropertyName("nullableString")] + public string? StringNullable { get; set; } = default!; + + [JsonPropertyName("decimalList")] + public List ListOfDecimal { get; set; } = default!; + + [JsonPropertyName("nullableDecimalList")] + public List ListOfNullableDecimal { get; set; } = default!; + + [JsonPropertyName("subList")] + public List SubTestModelList { get; set; } = default!; + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceTests.cs new file mode 100644 index 000000000..d49d5c82c --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceTests.cs @@ -0,0 +1,116 @@ +#nullable enable +using System.Text.Json.Serialization; +using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Validators; + +public class ValidationServiceTests +{ + private class MyModel + { + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("age")] + public int? Age { get; set; } + } + + private static readonly DataElement DefaultDataElement = new() + { + DataType = "MyType", + }; + + private static readonly DataType DefaultDataType = new() + { + Id = "MyType", + }; + + private readonly Mock> _loggerMock = new(); + private readonly Mock _dataClientMock = new(MockBehavior.Strict); + private readonly Mock _appModelMock = new(MockBehavior.Strict); + private readonly Mock _appMetadataMock = new(MockBehavior.Strict); + private readonly Mock _formDataValidatorMock = new(MockBehavior.Strict); + private readonly ServiceCollection _serviceCollection = new(); + + public ValidationServiceTests() + { + _serviceCollection.AddSingleton(_loggerMock.Object); + _serviceCollection.AddSingleton(_dataClientMock.Object); + _serviceCollection.AddSingleton(); + _serviceCollection.AddSingleton(_appModelMock.Object); + _serviceCollection.AddSingleton(_appMetadataMock.Object); + _serviceCollection.AddSingleton(_formDataValidatorMock.Object); + _formDataValidatorMock.Setup(v => v.DataType).Returns(DefaultDataType.Id); + _formDataValidatorMock.Setup(v => v.ValidationSource).Returns("MyNameValidator"); + } + + [Fact] + public async Task ValidateFormData_WithNoValidators_ReturnsNoErrors() + { + _serviceCollection.RemoveAll(typeof(IFormDataValidator)); + + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + + var validatorService = serviceProvider.GetRequiredService(); + var data = new MyModel { Name = "Ola" }; + var result = await validatorService.ValidateFormData(new Instance(), DefaultDataElement, DefaultDataType, data); + result.Should().BeEmpty(); + } + + [Fact] + public async Task ValidateFormData_WithMyNameValidator_ReturnsNoErrorsWhenNameIsOla() + { + _formDataValidatorMock.Setup(v => v.HasRelevantChanges(It.IsAny(), It.IsAny())).Returns(false); + _formDataValidatorMock.Setup(v => v.ValidateFormData(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync((Instance instance, DataElement dataElement, object data) => + { + if (data is MyModel model && model.Name != "Ola") + { + return new List { { new() { Severity = ValidationIssueSeverity.Error, CustomTextKey = "NameNotOla" } } }; + } + + return new List(); + }); + + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + + var validatorService = serviceProvider.GetRequiredService(); + var data = new MyModel { Name = "Ola" }; + var result = await validatorService.ValidateFormData(new Instance(), DefaultDataElement, DefaultDataType, data); + result.Should().ContainKey("MyNameValidator").WhoseValue.Should().HaveCount(0); + result.Should().HaveCount(1); + } + + [Fact] + public async Task ValidateFormData_WithMyNameValidator_ReturnsErrorsWhenNameIsKari() + { + _formDataValidatorMock.Setup(v => v.ValidateFormData(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync((Instance instance, DataElement dataElement, object data) => + { + if (data is MyModel model && model.Name != "Ola") + { + return new List { { new() { Severity = ValidationIssueSeverity.Error, CustomTextKey = "NameNotOla" } } }; + } + + return new List(); + }); + + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + + var validatorService = serviceProvider.GetRequiredService(); + var data = new MyModel { Name = "Kari" }; + var result = await validatorService.ValidateFormData(new Instance(), DefaultDataElement, DefaultDataType, data); + result.Should().ContainKey("MyNameValidator").WhoseValue.Should().ContainSingle().Which.CustomTextKey.Should().Be("NameNotOla"); + result.Should().HaveCount(1); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/hidden-field.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/hidden-field.json new file mode 100644 index 000000000..9ab1af8d5 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/hidden-field.json @@ -0,0 +1,39 @@ +{ + "name": "Should not return an error if component is hidden", + "expects": [], + "validationConfig": { + "$schema": "https://altinncdn.no/schemas/json/validation/validation.schema.v1.json", + "validations": { + "form.name": ["none-is-not-allowed"] + }, + "definitions": { + "none-is-not-allowed": { + "message": "none is not allowed", + "severity": "errors", + "condition": ["equals", ["dataModel", ["argv", 0]], "none"] + } + } + }, + "formData": { + "form": { + "name": "none" + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "name-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.name" + }, + "hidden": true + } + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/hidden-page.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/hidden-page.json new file mode 100644 index 000000000..57fd62f00 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/hidden-page.json @@ -0,0 +1,39 @@ +{ + "name": "Should not return an error if page is hidden", + "expects": [], + "validationConfig": { + "$schema": "https://altinncdn.no/schemas/json/validation/validation.schema.v1.json", + "validations": { + "form.name": ["none-is-not-allowed"] + }, + "definitions": { + "none-is-not-allowed": { + "message": "none is not allowed", + "severity": "errors", + "condition": ["equals", ["dataModel", ["argv", 0]], "none"] + } + } + }, + "formData": { + "form": { + "name": "none" + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "hidden": true, + "layout": [ + { + "id": "name-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.name" + } + } + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/many-errors.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/many-errors.json new file mode 100644 index 000000000..33f9604c7 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/many-errors.json @@ -0,0 +1,101 @@ +{ + "name": "Should return all of the correct errors", + "expects": [ + { + "message": "none is not allowed", + "severity": "errors", + "field": "form.name", + "componentId": "name-input" + }, + { + "message": "string is too short", + "severity": "errors", + "field": "form.name", + "componentId": "name-input" + }, + { + "message": "email must be real", + "severity": "errors", + "field": "form.email", + "componentId": "email-input" + }, + { + "message": "string is too short", + "severity": "errors", + "field": "form.email", + "componentId": "email-input" + } + ], + "validationConfig": { + "$schema": "https://altinncdn.no/schemas/json/validation/validation.schema.v1.json", + "validations": { + "form.name": [ + "none-is-not-allowed", + "str-len", + { + "message": "this should not be shown", + "severity": "warnings", + "condition": ["startsWith", ["dataModel", ["argv", 0]], "a"] + } + ], + "form.email": [ + "none-is-not-allowed", + { + "message": "email must be real", + "severity": "errors", + "condition": ["contains", ["dataModel", ["argv", 0]], "fake"] + }, + { + "ref": "str-len", + "condition": ["lessThan", ["stringLength", ["dataModel", ["argv", 0]]], 20] + }, + { + "message": "email must contain @", + "severity": "errors", + "condition": ["notContains", ["dataModel", ["argv", 0]], "@"] + } + ] + }, + "definitions": { + "none-is-not-allowed": { + "message": "none is not allowed", + "severity": "errors", + "condition": ["equals", ["dataModel", ["argv", 0]], "none"] + }, + "str-len": { + "message": "string is too short", + "severity": "errors", + "condition": ["lessThan", ["stringLength", ["dataModel", ["argv", 0]]], 5] + } + } + }, + "formData": { + "form": { + "name": "none", + "email": "fake@email.com" + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "name-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.name" + } + }, + { + "id": "email-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.email" + } + } + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden-row.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden-row.json new file mode 100644 index 000000000..04cc5de25 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden-row.json @@ -0,0 +1,177 @@ +{ + "name": "Should work in nested repeating groups with some hidden rows", + "expects": [ + { + "message": "zero is not allowed", + "severity": "errors", + "field": "form.people[1].number", + "componentId": "number-input-1" + }, + { + "message": "none is not allowed", + "severity": "errors", + "field": "form.people[1].names[2].name", + "componentId": "name-input-1-2" + }, + { + "message": "none is not allowed", + "severity": "errors", + "field": "form.people[2].names[2].name", + "componentId": "name-input-2-2" + } + ], + "validationConfig": { + "$schema": "https://altinncdn.no/schemas/json/validation/validation.schema.v1.json", + "validations": { + "form.people.number": ["zero-is-not-allowed"], + "form.people.names.name": ["none-is-not-allowed"] + }, + "definitions": { + "zero-is-not-allowed": { + "message": "zero is not allowed", + "severity": "errors", + "condition": ["equals", ["dataModel", ["argv", 0]], 0] + }, + "none-is-not-allowed": { + "message": "none is not allowed", + "severity": "errors", + "condition": ["equals", ["dataModel", ["argv", 0]], "none"] + } + } + }, + "formData": { + "form": { + "people": [ + { + "number": "0", + "hidden": true, + "names": [ + { + "name": "none", + "hidden": true + }, + { + "name": "John", + "hidden": false + }, + { + "name": "none", + "hidden": false + }, + { + "name": "Jane", + "hidden": true + } + ] + }, + { + "number": "0", + "hidden": false, + "names": [ + { + "name": "none", + "hidden": true + }, + { + "name": "John", + "hidden": false + }, + { + "name": "none", + "hidden": false + }, + { + "name": "Jane", + "hidden": true + } + ] + }, + { + "number": "1234", + "hidden": false, + "names": [ + { + "name": "none", + "hidden": true + }, + { + "name": "John", + "hidden": false + }, + { + "name": "none", + "hidden": false + }, + { + "name": "Jane", + "hidden": true + } + ] + }, + { + "number": "5678", + "hidden": true, + "names": [ + { + "name": "none", + "hidden": true + }, + { + "name": "John", + "hidden": false + }, + { + "name": "none", + "hidden": false + }, + { + "name": "Jane", + "hidden": true + } + ] + } + ] + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "people-group", + "type": "RepeatingGroup", + "dataModelBindings": { + "group": "form.people" + }, + "children": ["number-input", "names-group"], + "hiddenRow": ["dataModel", "form.people.hidden"] + }, + { + "id": "number-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.people.number" + } + }, + { + "id": "names-group", + "type": "RepeatingGroup", + "dataModelBindings": { + "group": "form.people.names" + }, + "children": ["name-input"], + "hiddenRow": ["dataModel", "form.people.names.hidden"] + }, + { + "id": "name-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.people.names.name" + } + } + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden.json new file mode 100644 index 000000000..c5700aefb --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden.json @@ -0,0 +1,189 @@ +{ + "name": "Should work in nested repeating groups with hidden fields in some rows", + "expects": [ + { + "message": "none is not allowed", + "severity": "errors", + "field": "form.people[0].names[2].name", + "componentId": "name-input-0-2" + }, + { + "message": "zero is not allowed", + "severity": "errors", + "field": "form.people[1].number", + "componentId": "number-input-1" + }, + { + "message": "none is not allowed", + "severity": "errors", + "field": "form.people[1].names[2].name", + "componentId": "name-input-1-2" + }, + { + "message": "none is not allowed", + "severity": "errors", + "field": "form.people[2].names[2].name", + "componentId": "name-input-2-2" + }, + { + "message": "none is not allowed", + "severity": "errors", + "field": "form.people[3].names[2].name", + "componentId": "name-input-3-2" + } + ], + "validationConfig": { + "$schema": "https://altinncdn.no/schemas/json/validation/validation.schema.v1.json", + "validations": { + "form.people.number": ["zero-is-not-allowed"], + "form.people.names.name": ["none-is-not-allowed"] + }, + "definitions": { + "zero-is-not-allowed": { + "message": "zero is not allowed", + "severity": "errors", + "condition": ["equals", ["dataModel", ["argv", 0]], 0] + }, + "none-is-not-allowed": { + "message": "none is not allowed", + "severity": "errors", + "condition": ["equals", ["dataModel", ["argv", 0]], "none"] + } + } + }, + "formData": { + "form": { + "people": [ + { + "number": "0", + "hidden": true, + "names": [ + { + "name": "none", + "hidden": true + }, + { + "name": "John", + "hidden": false + }, + { + "name": "none", + "hidden": false + }, + { + "name": "Jane", + "hidden": true + } + ] + }, + { + "number": "0", + "hidden": false, + "names": [ + { + "name": "none", + "hidden": true + }, + { + "name": "John", + "hidden": false + }, + { + "name": "none", + "hidden": false + }, + { + "name": "Jane", + "hidden": true + } + ] + }, + { + "number": "1234", + "hidden": false, + "names": [ + { + "name": "none", + "hidden": true + }, + { + "name": "John", + "hidden": false + }, + { + "name": "none", + "hidden": false + }, + { + "name": "Jane", + "hidden": true + } + ] + }, + { + "number": "5678", + "hidden": true, + "names": [ + { + "name": "none", + "hidden": true + }, + { + "name": "John", + "hidden": false + }, + { + "name": "none", + "hidden": false + }, + { + "name": "Jane", + "hidden": true + } + ] + } + ] + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "people-group", + "type": "RepeatingGroup", + "dataModelBindings": { + "group": "form.people" + }, + "children": ["number-input", "names-group"] + }, + { + "id": "number-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.people.number" + }, + "hidden": ["dataModel", "form.people.hidden"] + }, + { + "id": "names-group", + "type": "RepeatingGroup", + "dataModelBindings": { + "group": "form.people.names" + }, + "children": ["name-input"] + }, + { + "id": "name-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.people.names.name" + }, + "hidden": ["dataModel", "form.people.names.hidden"] + } + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/override.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/override.json new file mode 100644 index 000000000..299cf4d14 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/override.json @@ -0,0 +1,51 @@ +{ + "name": "Should return correct values when overriding a definition", + "expects": [ + { + "message": "this value could be wrong", + "severity": "warnings", + "field": "form.name", + "componentId": "name-input" + } + ], + "validationConfig": { + "$schema": "https://altinncdn.no/schemas/json/validation/validation.schema.v1.json", + "validations": { + "form.name": [ + { + "ref": "none-is-not-allowed", + "message": "this value could be wrong", + "severity": "warnings" + } + ] + }, + "definitions": { + "none-is-not-allowed": { + "message": "none is not allowed", + "severity": "errors", + "condition": ["equals", ["dataModel", ["argv", 0]], "none"] + } + } + }, + "formData": { + "form": { + "name": "none" + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "name-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.name" + } + } + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden-row.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden-row.json new file mode 100644 index 000000000..7b1ea3ec7 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden-row.json @@ -0,0 +1,71 @@ +{ + "name": "Should work in repeating groups with some hidden rows", + "expects": [ + { + "message": "none is not allowed", + "severity": "errors", + "field": "form.names[2].name", + "componentId": "name-input-2" + } + ], + "validationConfig": { + "$schema": "https://altinncdn.no/schemas/json/validation/validation.schema.v1.json", + "validations": { + "form.names.name": ["none-is-not-allowed"] + }, + "definitions": { + "none-is-not-allowed": { + "message": "none is not allowed", + "severity": "errors", + "condition": ["equals", ["dataModel", ["argv", 0]], "none"] + } + } + }, + "formData": { + "form": { + "names": [ + { + "name": "none", + "hidden": true + }, + { + "name": "John", + "hidden": false + }, + { + "name": "none", + "hidden": false + }, + { + "name": "Jane", + "hidden": true + } + ] + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "names-group", + "type": "RepeatingGroup", + "dataModelBindings": { + "group": "form.names" + }, + "children": ["name-input"], + "hiddenRow": ["dataModel", "form.names.hidden"] + }, + { + "id": "name-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.names.name" + } + } + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden.json new file mode 100644 index 000000000..63a9a4260 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden.json @@ -0,0 +1,71 @@ +{ + "name": "Should work in repeating groups with hidden fields in some rows", + "expects": [ + { + "message": "none is not allowed", + "severity": "errors", + "field": "form.names[2].name", + "componentId": "name-input-2" + } + ], + "validationConfig": { + "$schema": "https://altinncdn.no/schemas/json/validation/validation.schema.v1.json", + "validations": { + "form.names.name": ["none-is-not-allowed"] + }, + "definitions": { + "none-is-not-allowed": { + "message": "none is not allowed", + "severity": "errors", + "condition": ["equals", ["dataModel", ["argv", 0]], "none"] + } + } + }, + "formData": { + "form": { + "names": [ + { + "name": "none", + "hidden": true + }, + { + "name": "John", + "hidden": false + }, + { + "name": "none", + "hidden": false + }, + { + "name": "Jane", + "hidden": true + } + ] + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "names-group", + "type": "RepeatingGroup", + "dataModelBindings": { + "group": "form.names" + }, + "children": ["name-input"] + }, + { + "id": "name-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.names.name" + }, + "hidden": ["dataModel", "form.names.hidden"] + } + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating.json new file mode 100644 index 000000000..8899c0639 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating.json @@ -0,0 +1,72 @@ +{ + "name": "Should work in repeating groups", + "expects": [ + { + "message": "none is not allowed", + "severity": "errors", + "field": "form.names[0].name", + "componentId": "name-input-0" + }, + { + "message": "none is not allowed", + "severity": "errors", + "field": "form.names[2].name", + "componentId": "name-input-2" + } + ], + "validationConfig": { + "$schema": "https://altinncdn.no/schemas/json/validation/validation.schema.v1.json", + "validations": { + "form.names.name": ["none-is-not-allowed"] + }, + "definitions": { + "none-is-not-allowed": { + "message": "none is not allowed", + "severity": "errors", + "condition": ["equals", ["dataModel", ["argv", 0]], "none"] + } + } + }, + "formData": { + "form": { + "names": [ + { + "name": "none" + }, + { + "name": "John" + }, + { + "name": "none" + }, + { + "name": "Jane" + } + ] + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "names-group", + "type": "RepeatingGroup", + "dataModelBindings": { + "group": "form.names" + }, + "children": ["name-input"] + }, + { + "id": "name-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.names.name" + } + } + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/single-field-equals.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/single-field-equals.json new file mode 100644 index 000000000..0c4d725cf --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/single-field-equals.json @@ -0,0 +1,54 @@ +{ + "name": "Should return an error if equals condition is met", + "expects": [ + { + "message": "none is not allowed", + "severity": "errors", + "field": "form.name", + "componentId": "name-input" + } + ], + "validationConfig": { + "$schema": "https://altinncdn.no/schemas/json/validation/validation.schema.v1.json", + "validations": { + "form.name": ["none-is-not-allowed"], + "form.email": ["none-is-not-allowed"] + }, + "definitions": { + "none-is-not-allowed": { + "message": "none is not allowed", + "severity": "errors", + "condition": ["equals", ["dataModel", ["argv", 0]], "none"] + } + } + }, + "formData": { + "form": { + "name": "none", + "email": "email@address.com" + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "name-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.name" + } + }, + { + "id": "email-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.email" + } + } + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/warning.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/warning.json new file mode 100644 index 000000000..3e5a1bcbd --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/warning.json @@ -0,0 +1,44 @@ +{ + "name": "Should return a warning", + "expects": [ + { + "message": "none is not allowed", + "severity": "warnings", + "field": "form.name", + "componentId": "name-input" + } + ], + "validationConfig": { + "$schema": "https://altinncdn.no/schemas/json/validation/validation.schema.v1.json", + "validations": { + "form.name": [ + { + "message": "none is not allowed", + "severity": "warnings", + "condition": ["equals", ["dataModel", ["argv", 0]], "none"] + } + ] + } + }, + "formData": { + "form": { + "name": "none" + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "name-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.name" + } + } + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Helpers/JsonDataModel.cs b/test/Altinn.App.Core.Tests/Helpers/JsonDataModel.cs index f5bd722dd..6ba3888eb 100644 --- a/test/Altinn.App.Core.Tests/Helpers/JsonDataModel.cs +++ b/test/Altinn.App.Core.Tests/Helpers/JsonDataModel.cs @@ -1,12 +1,13 @@ #nullable enable using System.Text.Json; +using System.Text.Json.Nodes; using Altinn.App.Core.Helpers; using Altinn.App.Core.Helpers.DataModel; namespace Altinn.App.Core.Tests.Helpers; /// -/// Implementation of for data models based on JsonElement (mainliy for testing ) +/// Implementation of for data models based on JsonObject (mainliy for testing ) /// /// /// This class is written to enable the use of shared tests (with frontend) where the datamodel is defined @@ -14,12 +15,12 @@ namespace Altinn.App.Core.Tests.Helpers; /// public class JsonDataModel : IDataModelAccessor { - private readonly JsonElement? _modelRoot; + private readonly JsonObject? _modelRoot; /// - /// Constructor that creates a JsonDataModel based on a JsonElement + /// Constructor that creates a JsonDataModel based on a JsonObject /// - public JsonDataModel(JsonElement? modelRoot) + public JsonDataModel(JsonObject? modelRoot) { _modelRoot = modelRoot; } @@ -32,34 +33,43 @@ public JsonDataModel(JsonElement? modelRoot) return null; } - return GetModelDataRecursive(key.Split('.'), 0, _modelRoot.Value, indicies); + return GetModelDataRecursive(key.Split('.'), 0, _modelRoot, indicies); } - private object? GetModelDataRecursive(string[] keys, int index, JsonElement currentModel, ReadOnlySpan indicies) + private object? GetModelDataRecursive(string[] keys, int index, JsonNode? currentModel, ReadOnlySpan indicies) { + if (currentModel is null) + { + return null; + } + if (index == keys.Length) { - return currentModel.ValueKind switch + return currentModel switch { - JsonValueKind.String => currentModel.GetString(), - JsonValueKind.Number => currentModel.GetDouble(), - JsonValueKind.True => true, - JsonValueKind.False => false, - JsonValueKind.Object => null, - JsonValueKind.Array => null, - JsonValueKind.Null => null, - _ => throw new NotImplementedException($"Get Data is not implemented for {currentModel.ValueKind}"), + JsonValue jsonValue => jsonValue.GetValue().ValueKind switch + { + JsonValueKind.String => jsonValue.GetValue(), + JsonValueKind.Number => jsonValue.GetValue(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + _ => throw new NotImplementedException($"Get Data is not implemented for {jsonValue.GetType()}"), + }, + JsonObject obj => obj, + JsonArray arr => arr, + _ => throw new NotImplementedException($"Get Data is not implemented for {currentModel.GetType()}"), }; } var (key, groupIndex) = DataModel.ParseKeyPart(keys[index]); - if (currentModel.ValueKind != JsonValueKind.Object || !currentModel.TryGetProperty(key, out JsonElement childModel)) + if (currentModel is not JsonObject || !currentModel.AsObject().TryGetPropertyValue(key, out JsonNode? childModel)) { return null; } - if (childModel.ValueKind == JsonValueKind.Array) + if (childModel is JsonArray childArray) { if (groupIndex is null) { @@ -75,7 +85,7 @@ public JsonDataModel(JsonElement? modelRoot) indicies = default; // when you use a literal index, the context indecies are not to be used later. } - var arrayElement = childModel.EnumerateArray().ElementAt((int)groupIndex); + var arrayElement = childArray.ElementAt((int)groupIndex); return GetModelDataRecursive(keys, index + 1, arrayElement, indicies.Length > 0 ? indicies.Slice(1) : indicies); } @@ -90,28 +100,28 @@ public JsonDataModel(JsonElement? modelRoot) return null; } - return GetModelDataCountRecurs(key.Split('.'), 0, _modelRoot.Value, indicies); + return GetModelDataCountRecurs(key.Split('.'), 0, _modelRoot, indicies); } - private int? GetModelDataCountRecurs(string[] keys, int index, JsonElement currentModel, ReadOnlySpan indicies) + private int? GetModelDataCountRecurs(string[] keys, int index, JsonNode? currentModel, ReadOnlySpan indicies) { - if (index == keys.Length) + if (index == keys.Length || currentModel is null) { return null; // Last key part was not an JsonValueKind.Array } var (key, groupIndex) = DataModel.ParseKeyPart(keys[index]); - if (currentModel.ValueKind != JsonValueKind.Object || !currentModel.TryGetProperty(key, out JsonElement childModel)) + if (currentModel is not JsonObject || !currentModel.AsObject().TryGetPropertyValue(key, out JsonNode? childModel)) { return null; } - if (childModel.ValueKind == JsonValueKind.Array) + if (childModel is JsonArray childArray) { if (index == keys.Length - 1) { - return childModel.GetArrayLength(); + return childArray.Count; } if (groupIndex is null) @@ -128,24 +138,162 @@ public JsonDataModel(JsonElement? modelRoot) indicies = default; // when you use a literal index, the context indecies are not to be used later. } - var arrayElement = childModel.EnumerateArray().ElementAt((int)groupIndex); + var arrayElement = childArray.ElementAt((int)groupIndex); return GetModelDataCountRecurs(keys, index + 1, arrayElement, indicies.Length > 0 ? indicies.Slice(1) : indicies); } return GetModelDataCountRecurs(keys, index + 1, childModel, indicies); } + /// + public string[] GetResolvedKeys(string key) + { + if (_modelRoot is null) + { + return new string[0]; + } + + var keyParts = key.Split('.'); + return GetResolvedKeysRecursive(keyParts, _modelRoot); + } + + private string[] GetResolvedKeysRecursive(string[] keyParts, JsonNode? currentModel, int currentIndex = 0, string currentKey = "") + { + if (currentModel is null) + { + return new string[0]; + } + + if (currentIndex == keyParts.Length) + { + return new[] { currentKey }; + } + + var (key, groupIndex) = DataModel.ParseKeyPart(keyParts[currentIndex]); + if (currentModel is not JsonObject || !currentModel.AsObject().TryGetPropertyValue(key, out JsonNode? childModel)) + { + return new string[0]; + } + + if (childModel is JsonArray childArray) + { + // childModel is an array + if (groupIndex is null) + { + // Index not specified, recurse on all elements + int i = 0; + var resolvedKeys = new List(); + foreach (var child in childArray) + { + var newResolvedKeys = GetResolvedKeysRecursive(keyParts, child, currentIndex + 1, DataModel.JoinFieldKeyParts(currentKey, key + "[" + i + "]")); + resolvedKeys.AddRange(newResolvedKeys); + i++; + } + + return resolvedKeys.ToArray(); + } + else + { + // Index specified, recurse on that element + return GetResolvedKeysRecursive(keyParts, childModel, currentIndex + 1, DataModel.JoinFieldKeyParts(currentKey, key + "[" + groupIndex + "]")); + } + } + + // Otherwise, just recurse + return GetResolvedKeysRecursive(keyParts, childModel, currentIndex + 1, DataModel.JoinFieldKeyParts(currentKey, key)); + + } + /// public string AddIndicies(string key, ReadOnlySpan indicies = default) { - // We don't have a schema for the datamodel in Json - throw new NotImplementedException(); + if (indicies.Length == 0) + { + return key; + } + + var keys = key.Split('.'); + var outputKey = string.Empty; + JsonNode? currentModel = _modelRoot; + + foreach (var keyPart in keys) + { + var (currentKey, groupIndex) = DataModel.ParseKeyPart(keyPart); + var currentIndex = groupIndex ?? (indicies.Length > 0 ? indicies[0] : null); + + if (currentModel is not JsonObject currentObject) + { + throw new DataModelException("Cannot access property of a JsonValue or JsonArray"); + } + + if (!currentObject.TryGetPropertyValue(currentKey, out JsonNode? childModel)) + { + throw new DataModelException($"Cannot find property {currentKey} in {currentObject}"); + } + + if (childModel is JsonArray childArray && currentIndex is not null) + { + outputKey = DataModel.JoinFieldKeyParts(outputKey, currentKey + "[" + currentIndex + "]"); + currentModel = childArray.ElementAt((int)currentIndex); + if (indicies.Length > 0) + { + indicies = indicies.Slice(1); + } + } + else + { + if (groupIndex is not null) + { + throw new DataModelException("Index on non indexable property"); + } + + outputKey = DataModel.JoinFieldKeyParts(outputKey, currentKey); + currentModel = childModel; + } + } + + return outputKey; } /// public void RemoveField(string key, RowRemovalOption rowRemovalOption) { - throw new NotImplementedException("Impossible to remove fields in a json model"); + var keys_split = key.Split('.'); + var keys = keys_split[0..^1]; + var (lastKey, lastGroupIndex) = DataModel.ParseKeyPart(keys_split[^1]); + + object? modelData = GetModelDataRecursive(keys, 0, _modelRoot, default); + if (modelData is not JsonObject containingObject) + { + return; + } + + if (lastGroupIndex is not null) + { + // Remove row from list + if (!(containingObject.TryGetPropertyValue(lastKey, out JsonNode? childModel) && childModel is JsonArray childArray)) + { + throw new ArgumentException($"Tried to remove row {key}, ended in a non-list"); + } + + switch (rowRemovalOption) + { + case RowRemovalOption.DeleteRow: + childArray.RemoveAt((int)lastGroupIndex); + break; + case RowRemovalOption.SetToNull: + childArray[(int)lastGroupIndex] = null; + break; + case RowRemovalOption.Ignore: + return; + } + } + else + { + // Set the property to null + containingObject[lastKey] = null; + } + } /// diff --git a/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs b/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs index a994def27..d4109b727 100644 --- a/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs @@ -23,12 +23,12 @@ public class JsonHelperTests var logger = new Mock().Object; var guid = Guid.Empty; var dataProcessorMock = new Mock(); - Func> dataProcessWrite = (instance, guid, model) => Task.FromResult(processDataWriteImpl((TModel)model)); + Func> dataProcessWrite = (instance, guid, model, previousModel) => Task.FromResult(processDataWriteImpl((TModel)model)); dataProcessorMock - .Setup((d) => d.ProcessDataWrite(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup((d) => d.ProcessDataWrite(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(dataProcessWrite); - return await JsonHelper.ProcessDataWriteWithDiff(instance, guid, model, dataProcessorMock.Object, logger); + return await JsonHelper.ProcessDataWriteWithDiff(instance, guid, model, new IDataProcessor[] { dataProcessorMock.Object }, logger); } public class TestModel @@ -84,8 +84,7 @@ public async Task InitializingPropertiesLeadsToNoDiff() return true; }); - // Might be null in the future - diff.Should().BeEmpty(); + diff.Should().BeNull(); } [Fact] diff --git a/test/Altinn.App.Core.Tests/Helpers/LinqExpressionHelpersTests.cs b/test/Altinn.App.Core.Tests/Helpers/LinqExpressionHelpersTests.cs new file mode 100644 index 000000000..cd2152ff2 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/LinqExpressionHelpersTests.cs @@ -0,0 +1,68 @@ +#nullable enable +using System.Text.Json.Serialization; +using Altinn.App.Core.Helpers; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Helpers; + +public class LinqExpressionHelpersTests +{ + public class MyModel + { + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("age")] + public int? Age { get; set; } + + public List? Children { get; set; } + } + + [Fact] + public void GetJsonPath_OneLevelDeep() + { + var propertyName = LinqExpressionHelpers.GetJsonPath(m => m.Name); + propertyName.Should().Be("name"); + } + + [Fact] + public void GetJsonPath_TwoLevelsDeep() + { + var propertyName = LinqExpressionHelpers.GetJsonPath(m => m.Children![0].Age); + propertyName.Should().Be("Children[0].age"); + } + + [Fact()] + public void GetJsonPath_TwoLevelsDeepUsingFirst() + { + var propertyName = LinqExpressionHelpers.GetJsonPath>(m => m.Children!.Select(c => c.Age)); + propertyName.Should().Be("Children.age"); + } + + [Fact] + public void GetJsonPath_ManyLevelsDeep() + { + var propertyName = LinqExpressionHelpers.GetJsonPath>(m => m.Children![0].Children![2].Children!.Select(c => c.Children![44].Age)); + propertyName.Should().Be("Children[0].Children[2].Children.Children[44].age"); + } + + [Fact] + public void GetJsonPath_IndexInVariable() + { + var index = 123; + var propertyName = LinqExpressionHelpers.GetJsonPath(m => m.Children![index].Age); + propertyName.Should().Be("Children[123].age"); + } + + [Fact] + public void GetJsonPath_IndexInVariableLoop() + { + for (var i = 0; i < 10; i++) + { + var index = i; // Needed to avoid "Access to modified closure" error + var propertyName = LinqExpressionHelpers.GetJsonPath(m => m.Children![index].Age); + propertyName.Should().Be($"Children[{index}].age"); + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Helpers/MultiDecisionHelperTests.cs b/test/Altinn.App.Core.Tests/Helpers/MultiDecisionHelperTests.cs new file mode 100644 index 000000000..20d60e5e3 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/MultiDecisionHelperTests.cs @@ -0,0 +1,265 @@ +using System.Security.Claims; +using System.Text.Json; +using Altinn.App.Core.Helpers; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Helpers; + +public class MultiDecisionHelperTests +{ + private static readonly JsonSerializerOptions SerializerOptions = new() + { + WriteIndented = true + }; + + [Fact] + public void CreateMultiDecisionRequest_generates_multidecisionrequest_with_all_actions_current_task_elemtnId() + { + var claimsPrincipal = GetClaims("1337"); + + var instance = new Instance() + { + Id = "1337/1dd16477-187b-463c-8adf-592c7fa78459", + Org = "tdd", + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + AppId = "tdd/test-app", + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + AltinnTaskType = "Data", + ElementId = "Task_1" + }, + EndEvent = "EndEvent_1" + } + }; + + var actions = new List() + { + "sign", + "reject" + }; + + var result = MultiDecisionHelper.CreateMultiDecisionRequest(claimsPrincipal, instance, actions); + + CompareWithOrUpdateGoldenFile("multidecision-all-actions-task", result); + } + + [Fact] + public void CreateMultiDecisionRequest_generates_multidecisionrequest_with_all_actions_instanceId_is_GUID_only() + { + var claimsPrincipal = GetClaims("1337"); + + var instance = new Instance() + { + Id = "1dd16477-187b-463c-8adf-592c7fa78459", + Org = "tdd", + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + AppId = "tdd/test-app", + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + AltinnTaskType = "Data", + ElementId = "Task_1" + }, + EndEvent = "EndEvent_1" + } + }; + + var actions = new List() + { + "sign", + "reject" + }; + + var result = MultiDecisionHelper.CreateMultiDecisionRequest(claimsPrincipal, instance, actions); + + CompareWithOrUpdateGoldenFile("multidecision-all-actions-guid", result); + } + + [Fact] + public void CreateMultiDecisionRequest_generates_multidecisionrequest_with_all_actions_endevent() + { + var claimsPrincipal = GetClaims("1337"); + + var instance = new Instance() + { + Id = "1337/1dd16477-187b-463c-8adf-592c7fa78459", + Org = "tdd", + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + AppId = "tdd/test-app", + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + AltinnTaskType = "Task_1" + }, + EndEvent = "EndEvent_1" + } + }; + + var actions = new List() + { + "sign", + "reject" + }; + + var result = MultiDecisionHelper.CreateMultiDecisionRequest(claimsPrincipal, instance, actions); + + CompareWithOrUpdateGoldenFile("multidecision-all-actions-endevent", result); + } + + [Fact] + public void CreateMultiDecisionRequest_throws_ArgumentNullException_if_user_is_null() + { + var instance = new Instance() + { + Id = "1337/1dd16477-187b-463c-8adf-592c7fa78459", + Org = "tdd", + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + AppId = "tdd/test-app", + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + AltinnTaskType = "Task_1" + }, + EndEvent = "EndEvent_1" + } + }; + + var actions = new List() + { + "sign", + "reject" + }; + Action act = () => MultiDecisionHelper.CreateMultiDecisionRequest(null, instance, actions); + act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'user')"); + } + + [Fact] + public void ValidateDecisionResult_all_actions_allowed() + { + var response = GetXacmlJsonRespons("all-actions-allowed"); + var expected = new Dictionary() + { + { "read", true }, + { "write", true }, + { "complete", true }, + { "lookup", true } + }; + var actions = new Dictionary() + { + { "read", false }, + { "write", false }, + { "complete", false }, + { "lookup", false } + }; + var result = MultiDecisionHelper.ValidatePdpMultiDecision(actions, response, GetClaims("501337")); + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public void ValidateDecisionResult_one_action_denied() + { + var response = GetXacmlJsonRespons("one-action-denied"); + var expected = new Dictionary() + { + { "read", true }, + { "write", true }, + { "complete", true }, + { "lookup", false } + }; + var actions = new Dictionary() + { + { "read", false }, + { "write", false }, + { "complete", false }, + { "lookup", false } + }; + var result = MultiDecisionHelper.ValidatePdpMultiDecision(actions, response, GetClaims("501337")); + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public void ValidateDecisionResult_throws_ArgumentNullException_if_response_is_null() + { + var actions = new Dictionary() + { + { "read", false }, + { "write", false }, + { "complete", false }, + { "lookup", false } + }; + Action act = () => MultiDecisionHelper.ValidatePdpMultiDecision(actions, null, GetClaims("501337")); + act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'results')"); + } + + [Fact] + public void ValidateDecisionResult_throws_ArgumentNullException_if_user_is_null() + { + var response = GetXacmlJsonRespons("one-action-denied"); + var actions = new Dictionary() + { + { "read", false }, + { "write", false }, + { "complete", false }, + { "lookup", false } + }; + Action act = () => MultiDecisionHelper.ValidatePdpMultiDecision(actions, response, null); + act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'user')"); + } + + private static ClaimsPrincipal GetClaims(string partyId) + { + return new ClaimsPrincipal(new List() + { + new(new List + { + new("urn:altinn:partyid", partyId, "#integer"), + new("urn:altinn:authlevel", "3", "#integer"), + }) + }); + } + + private static string XacmlJsonRequestRootToString(XacmlJsonRequestRoot request) + { + return JsonSerializer.Serialize(request, SerializerOptions); + } + + private static void CompareWithOrUpdateGoldenFile(string testId, XacmlJsonRequestRoot xacmlJsonRequestRoot) + { + bool updateGoldeFiles = Environment.GetEnvironmentVariable("UpdateGoldenFiles") == "true"; + string goldenFilePath = Path.Join("Helpers", "TestData", "MultiDecisionHelper", testId + ".golden.json"); + string xacmlJsonRequestRootAsString = XacmlJsonRequestRootToString(xacmlJsonRequestRoot); + if (updateGoldeFiles) + { + File.WriteAllText(goldenFilePath, xacmlJsonRequestRootAsString); + } + + string goldenFileContent = File.ReadAllText(goldenFilePath); + Assert.Equal(goldenFileContent, xacmlJsonRequestRootAsString); + } + + private static List GetXacmlJsonRespons(string filename) + { + var xacmlJesonRespons = File.ReadAllText(Path.Join("Helpers", "TestData", "MultiDecisionHelper", filename + ".json")); + return JsonSerializer.Deserialize>(xacmlJesonRespons); + } +} diff --git a/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs b/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs new file mode 100644 index 000000000..1662fe5c8 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs @@ -0,0 +1,105 @@ +#nullable enable +using Altinn.App.Core.Helpers; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Helpers; + +public class ObjectUtilsTests +{ + public class TestClass + { + public string? StringValue { get; set; } + + public decimal Decimal { get; set; } + + public decimal? NullableDecimal { get; set; } + + public TestClass? Child { get; set; } + + public List? Children { get; set; } + } + + [Fact] + public void TestSimple() + { + var test = new TestClass(); + test.Children.Should().BeNull(); + + ObjectUtils.InitializeListsAndNullEmptyStrings(test); + + test.Children.Should().BeEmpty(); + } + + [Fact] + public void TestSimpleStringInitialized() + { + var test = new TestClass() + { + StringValue = "some", + }; + test.Children.Should().BeNull(); + + ObjectUtils.InitializeListsAndNullEmptyStrings(test); + + test.Children.Should().BeEmpty(); + test.StringValue.Should().Be("some"); + } + + [Fact] + public void TestSimpleListInitialized() + { + var test = new TestClass() + { + Children = new(), + }; + test.Children.Should().BeEmpty(); + + ObjectUtils.InitializeListsAndNullEmptyStrings(test); + + test.Children.Should().BeEmpty(); + } + + [Fact] + public void TestMultipleLevelsInitialized() + { + var test = new TestClass() + { + Child = new TestClass() + { + Child = new TestClass() + { + Child = new TestClass() + { + Children = new() + { + new TestClass() + { + Child = new TestClass() + } + } + } + } + } + }; + test.Children.Should().BeNull(); + test.Child.Children.Should().BeNull(); + test.Child.Child.Children.Should().BeNull(); + var subChild = test.Child.Child.Child.Children.Should().ContainSingle().Which; + subChild.Children.Should().BeNull(); + subChild.Child.Should().NotBeNull(); + subChild.Child!.Children.Should().BeNull(); + + // Act + ObjectUtils.InitializeListsAndNullEmptyStrings(test); + + // Assert + test.Children.Should().BeEmpty(); + test.Child.Children.Should().BeEmpty(); + test.Child.Child.Children.Should().BeEmpty(); + subChild = test.Child.Child.Child.Children.Should().ContainSingle().Which; + subChild.Children.Should().BeEmpty(); + subChild.Child.Should().NotBeNull(); + subChild.Child!.Children.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/all-actions-allowed.json b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/all-actions-allowed.json new file mode 100644 index 000000000..f4435cf59 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/all-actions-allowed.json @@ -0,0 +1,230 @@ +[ + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "read", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "write", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "complete", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "lookup", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + } +] \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-endevent.golden.json b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-endevent.golden.json new file mode 100644 index 000000000..ac3952e0c --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-endevent.golden.json @@ -0,0 +1,126 @@ +{ + "Request": { + "ReturnPolicyIdList": false, + "CombinedDecision": false, + "XPathVersion": null, + "Category": null, + "Resource": [ + { + "CategoryId": null, + "Id": "r1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:end-event", + "Value": "EndEvent_1", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:instance-id", + "Value": "1337/1dd16477-187b-463c-8adf-592c7fa78459", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + }, + { + "AttributeId": "urn:altinn:partyid", + "Value": "1337", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:org", + "Value": "tdd", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:app", + "Value": "test-app", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + } + ] + } + ], + "Action": [ + { + "CategoryId": null, + "Id": "a1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "sign", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + } + ] + }, + { + "CategoryId": null, + "Id": "a2", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "reject", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + } + ] + } + ], + "AccessSubject": [ + { + "CategoryId": null, + "Id": "s1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:partyid", + "Value": "1337", + "Issuer": "LOCAL AUTHORITY", + "DataType": "#integer", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:authlevel", + "Value": "3", + "Issuer": "LOCAL AUTHORITY", + "DataType": "#integer", + "IncludeInResult": false + } + ] + } + ], + "RecipientSubject": null, + "IntermediarySubject": null, + "RequestingMachine": null, + "MultiRequests": { + "RequestReference": [ + { + "ReferenceId": [ + "s1", + "a1", + "r1" + ] + }, + { + "ReferenceId": [ + "s1", + "a2", + "r1" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-guid.golden.json b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-guid.golden.json new file mode 100644 index 000000000..0acdb5dc3 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-guid.golden.json @@ -0,0 +1,126 @@ +{ + "Request": { + "ReturnPolicyIdList": false, + "CombinedDecision": false, + "XPathVersion": null, + "Category": null, + "Resource": [ + { + "CategoryId": null, + "Id": "r1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:task", + "Value": "Task_1", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:instance-id", + "Value": "1337/1dd16477-187b-463c-8adf-592c7fa78459", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + }, + { + "AttributeId": "urn:altinn:partyid", + "Value": "1337", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:org", + "Value": "tdd", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:app", + "Value": "test-app", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + } + ] + } + ], + "Action": [ + { + "CategoryId": null, + "Id": "a1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "sign", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + } + ] + }, + { + "CategoryId": null, + "Id": "a2", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "reject", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + } + ] + } + ], + "AccessSubject": [ + { + "CategoryId": null, + "Id": "s1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:partyid", + "Value": "1337", + "Issuer": "LOCAL AUTHORITY", + "DataType": "#integer", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:authlevel", + "Value": "3", + "Issuer": "LOCAL AUTHORITY", + "DataType": "#integer", + "IncludeInResult": false + } + ] + } + ], + "RecipientSubject": null, + "IntermediarySubject": null, + "RequestingMachine": null, + "MultiRequests": { + "RequestReference": [ + { + "ReferenceId": [ + "s1", + "a1", + "r1" + ] + }, + { + "ReferenceId": [ + "s1", + "a2", + "r1" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-task.golden.json b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-task.golden.json new file mode 100644 index 000000000..0acdb5dc3 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-task.golden.json @@ -0,0 +1,126 @@ +{ + "Request": { + "ReturnPolicyIdList": false, + "CombinedDecision": false, + "XPathVersion": null, + "Category": null, + "Resource": [ + { + "CategoryId": null, + "Id": "r1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:task", + "Value": "Task_1", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:instance-id", + "Value": "1337/1dd16477-187b-463c-8adf-592c7fa78459", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + }, + { + "AttributeId": "urn:altinn:partyid", + "Value": "1337", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:org", + "Value": "tdd", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:app", + "Value": "test-app", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + } + ] + } + ], + "Action": [ + { + "CategoryId": null, + "Id": "a1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "sign", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + } + ] + }, + { + "CategoryId": null, + "Id": "a2", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "reject", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + } + ] + } + ], + "AccessSubject": [ + { + "CategoryId": null, + "Id": "s1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:partyid", + "Value": "1337", + "Issuer": "LOCAL AUTHORITY", + "DataType": "#integer", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:authlevel", + "Value": "3", + "Issuer": "LOCAL AUTHORITY", + "DataType": "#integer", + "IncludeInResult": false + } + ] + } + ], + "RecipientSubject": null, + "IntermediarySubject": null, + "RequestingMachine": null, + "MultiRequests": { + "RequestReference": [ + { + "ReferenceId": [ + "s1", + "a1", + "r1" + ] + }, + { + "ReferenceId": [ + "s1", + "a2", + "r1" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/one-action-denied.json b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/one-action-denied.json new file mode 100644 index 000000000..d8eff9c09 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/one-action-denied.json @@ -0,0 +1,188 @@ +[ + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "read", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "write", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "complete", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "NotApplicable", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": null, + "AssociateAdvice": null, + "Category": null, + "PolicyIdentifierList": null + } +] diff --git a/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs b/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs index 8987e91aa..fe5e52b67 100644 --- a/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs @@ -1,6 +1,5 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Implementation; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; diff --git a/test/Altinn.App.Core.Tests/Implementation/DefaultPageOrderTest.cs b/test/Altinn.App.Core.Tests/Implementation/DefaultPageOrderTest.cs index 571daa7d0..6fb626122 100644 --- a/test/Altinn.App.Core.Tests/Implementation/DefaultPageOrderTest.cs +++ b/test/Altinn.App.Core.Tests/Implementation/DefaultPageOrderTest.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Altinn.App.Core.Features.PageOrder; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; using Moq; using Xunit; diff --git a/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs b/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs index d0ff08e61..bec09e046 100644 --- a/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs @@ -1,15 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Altinn.App.Core.Features; using Altinn.App.Core.Implementation; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Pdf; +using Altinn.App.Core.Internal.Prefill; using Altinn.App.Core.Models; using Altinn.App.Core.Tests.Implementation.TestData.AppDataModel; +using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -25,12 +25,12 @@ public class DefaultTaskEventsTests: IDisposable private readonly Mock _resMock; private readonly Mock _metaMock; private readonly ApplicationMetadata _application; - private readonly Mock _dataMock; + private readonly Mock _dataMock; private readonly Mock _prefillMock; private readonly IAppModel _appModel; private readonly Mock _appModelMock; private readonly Mock _instantiationMock; - private readonly Mock _instanceMock; + private readonly Mock _instanceMock; private IEnumerable _taskStarts; private IEnumerable _taskEnds; private IEnumerable _taskAbandons; @@ -43,12 +43,12 @@ public DefaultTaskEventsTests() _application = new ApplicationMetadata("ttd/test"); _resMock = new Mock(); _metaMock = new Mock(); - _dataMock = new Mock(); + _dataMock = new Mock(); _prefillMock = new Mock(); _appModel = new DefaultAppModel(NullLogger.Instance); _appModelMock = new Mock(); _instantiationMock = new Mock(); - _instanceMock = new Mock(); + _instanceMock = new Mock(); _taskStarts = new List(); _taskEnds = new List(); _taskAbandons = new List(); @@ -134,7 +134,8 @@ public async void OnEndProcessTask_calls_all_added_implementations_of_IProcessTa _layoutStateInitializer); var instance = new Instance() { - Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d" + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", }; await te.OnEndProcessTask("Task_1", instance); _metaMock.Verify(r => r.GetApplicationMetadata()); @@ -196,7 +197,7 @@ public async void OnEndProcessTask_removes_all_shadow_fields_and_saves_to_specif _metaMock.Verify(r => r.GetApplicationMetadata()); _dataMock.Verify(r => r.InsertFormData(It.IsAny(), instanceGuid, modelType, "ttd", "shadow-fields-test", 1000, "model-clean")); _dataMock.Verify(r => r.GetFormData(instanceGuid, modelType, "ttd", "shadow-fields-test", 1000, dataElementId)); - _dataMock.Verify(r => r.Update(instance, instance.Data[0])); + _dataMock.Verify(r => r.LockDataElement(It.Is(i => i.InstanceOwnerPartyId == 1337 && i.InstanceGuid == instanceGuid), new Guid(instance.Data[0].Id))); } [Fact] @@ -251,7 +252,7 @@ public async void OnEndProcessTask_removes_all_shadow_fields_and_saves_to_curren _metaMock.Verify(r => r.GetApplicationMetadata()); _dataMock.Verify(r => r.UpdateData(It.IsAny(), instanceGuid, modelType, "ttd", "shadow-fields-test", 1000, dataElementId)); _dataMock.Verify(r => r.GetFormData(instanceGuid, modelType, "ttd", "shadow-fields-test", 1000, dataElementId)); - _dataMock.Verify(r => r.Update(instance, instance.Data[0])); + _dataMock.Verify(r => r.LockDataElement(It.Is(i => i.InstanceOwnerPartyId == 1337 && i.InstanceGuid == instanceGuid), new Guid(instance.Data[0].Id))); } [Fact] @@ -373,6 +374,7 @@ public async void OnEndProcessTask_does_not_sets_hard_soft_delete_if_process_end var instance = new Instance() { Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", InstanceOwner = new() { PartyId = "1000" @@ -387,6 +389,68 @@ public async void OnEndProcessTask_does_not_sets_hard_soft_delete_if_process_end _instanceMock.Verify(i => i.DeleteInstance(1000, Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"), true), Times.Never); } + [Fact] + public async void OnEndProcessTask_deletes_old_datatypes_generated_from_task_beeing_ended() + { + _application.DataTypes = new List(); + _application.AutoDeleteOnProcessEnd = false; + _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(_application); + DefaultTaskEvents te = new DefaultTaskEvents( + _logger, + _resMock.Object, + _metaMock.Object, + _dataMock.Object, + _prefillMock.Object, + _appModel, + _instantiationMock.Object, + _instanceMock.Object, + _taskStarts, + _taskEnds, + _taskAbandons, + _pdfMock.Object, + _featureManagerMock.Object, + _layoutStateInitializer); + var instance = new Instance() + { + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", + InstanceOwner = new() + { + PartyId = "1000" + }, + Process = new() + { + Ended = DateTime.Now + }, + Data = new() + { + new() + { + Id = "ba0678ad-960d-4307-aba2-ba29c9804c9d", + References = new() + { + new() + { + Relation = RelationType.GeneratedFrom, + Value = "Task_1", + ValueType = ReferenceType.Task + }, + new() + { + Relation = RelationType.GeneratedFrom, + Value = "EndEvent_1", + ValueType = ReferenceType.Task + } + } + } + } + }; + await te.OnEndProcessTask("EndEvent_1", instance); + _metaMock.Verify(r => r.GetApplicationMetadata()); + _instanceMock.Verify(i => i.DeleteInstance(1000, Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"), true), Times.Never); + _dataMock.Verify(d => d.DeleteData("ttd", "test", 1337, Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"), Guid.Parse("ba0678ad-960d-4307-aba2-ba29c9804c9d"), false), Times.Once); + } + [Fact] public async void OnEndProcessTask_sets_hard_soft_delete_if_process_ended_and_autoDeleteOnProcessEnd_true() { @@ -411,6 +475,7 @@ public async void OnEndProcessTask_sets_hard_soft_delete_if_process_ended_and_au var instance = new Instance() { Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", InstanceOwner = new() { PartyId = "1000" @@ -449,6 +514,7 @@ public async void OnEndProcessTask_does_not_sets_hard_soft_delete_if_process_not var instance = new Instance() { Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", InstanceOwner = new() { PartyId = "1000" @@ -484,6 +550,7 @@ public async void OnEndProcessTask_does_not_sets_hard_soft_delete_if_process_nul var instance = new Instance() { Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", InstanceOwner = new() { PartyId = "1000" diff --git a/test/Altinn.App.Core.Tests/Implementation/NullDataProcessorTests.cs b/test/Altinn.App.Core.Tests/Implementation/NullDataProcessorTests.cs deleted file mode 100644 index 39e705c84..000000000 --- a/test/Altinn.App.Core.Tests/Implementation/NullDataProcessorTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Altinn.App.Core.Features.DataProcessing; -using Altinn.App.PlatformServices.Tests.Implementation.TestResources; -using Altinn.Platform.Storage.Interface.Models; -using FluentAssertions; -using Xunit; - -namespace Altinn.App.PlatformServices.Tests.Implementation; - -public class NullDataProcessorTests -{ - [Fact] - public async void NullDataProcessor_ProcessDataRead_makes_no_changes_and_returns_false() - { - // Arrange - var dataProcessor = new NullDataProcessor(); - DummyModel expected = new DummyModel() - { - Name = "Test" - }; - object input = new DummyModel() - { - Name = "Test" - }; - - // Act - var result = await dataProcessor.ProcessDataRead(new Instance(), null, input); - - // Assert - result.Should().BeFalse(); - input.Should().BeEquivalentTo(expected); - } - - [Fact] - public async void NullDataProcessor_ProcessDataWrite_makes_no_changes_and_returns_false() - { - // Arrange - var dataProcessor = new NullDataProcessor(); - DummyModel expected = new DummyModel() - { - Name = "Test" - }; - object input = new DummyModel() - { - Name = "Test" - }; - - // Act - var result = await dataProcessor.ProcessDataWrite(new Instance(), null, input); - - // Assert - result.Should().BeFalse(); - input.Should().BeEquivalentTo(expected); - } -} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Implementation/NullInstanceValidatorTests.cs b/test/Altinn.App.Core.Tests/Implementation/NullInstanceValidatorTests.cs deleted file mode 100644 index a421a8281..000000000 --- a/test/Altinn.App.Core.Tests/Implementation/NullInstanceValidatorTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Altinn.App.Core.Features.Validation; -using Altinn.App.PlatformServices.Tests.Implementation.TestResources; -using Altinn.Platform.Storage.Interface.Models; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Xunit; - -namespace Altinn.App.PlatformServices.Tests.Implementation; - -public class NullInstanceValidatorTests -{ - [Fact] - public async void NullInstanceValidator_ValidateData_does_not_add_to_ValidationResults() - { - // Arrange - var instanceValidator = new NullInstanceValidator(); - ModelStateDictionary validationResults = new ModelStateDictionary(); - - // Act - await instanceValidator.ValidateData(new DummyModel(), validationResults); - - // Assert - Assert.Empty(validationResults); - } - - [Fact] - public async void NullInstanceValidator_ValidateTask_does_not_add_to_ValidationResults() - { - // Arrange - var instanceValidator = new NullInstanceValidator(); - ModelStateDictionary validationResults = new ModelStateDictionary(); - - // Act - await instanceValidator.ValidateTask(new Instance(), "task0", validationResults); - - // Assert - Assert.Empty(validationResults); - } -} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs b/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs index f2fa4c501..71861bbe1 100644 --- a/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs @@ -5,8 +5,8 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; using Altinn.App.Core.Infrastructure.Clients.Register; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Models; using Altinn.App.PlatformServices.Tests.Mocks; using Altinn.Common.AccessTokenClient.Services; diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs new file mode 100644 index 000000000..55ce279e2 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs @@ -0,0 +1,132 @@ +using System.Runtime.InteropServices.JavaScript; +using System.Security.Claims; +using System.Text.Json; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Infrastructure.Clients.Authorization; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Interfaces; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Infrastructure.Clients.Authorization; + +public class AuthorizationClientTests +{ + [Fact] + public async Task AuthorizeActions_returns_dictionary_with_one_action_denied() + { + Mock pdpMock = new(); + Mock httpContextAccessorMock = new(); + Mock httpClientMock = new(); + Mock> appSettingsMock = new(); + var pdpResponse = GetXacmlJsonRespons("one-action-denied"); + pdpMock.Setup(s => s.GetDecisionForRequest(It.IsAny())).ReturnsAsync(pdpResponse); + AuthorizationClient client = new AuthorizationClient(Options.Create(new PlatformSettings()), httpContextAccessorMock.Object, httpClientMock.Object, appSettingsMock.Object, pdpMock.Object, NullLogger.Instance); + + var claimsPrincipal = GetClaims("1337"); + + var instance = new Instance() + { + Id = "1337/1dd16477-187b-463c-8adf-592c7fa78459", + Org = "tdd", + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + AppId = "tdd/test-app", + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + AltinnTaskType = "Data", + ElementId = "Task_1" + }, + EndEvent = "EndEvent_1" + } + }; + + var expected = new Dictionary() + { + { "read", true }, + { "write", true }, + { "complete", true }, + { "lookup", false } + }; + var actions = new List() + { + "read", + "write", + "complete", + "lookup" + }; + var actual = await client.AuthorizeActions(instance, claimsPrincipal, actions); + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task AuthorizeActions_returns_empty_dictionary_if_no_response_from_pdp() + { + Mock pdpMock = new(); + Mock httpContextAccessorMock = new(); + Mock httpClientMock = new(); + Mock> appSettingsMock = new(); + pdpMock.Setup(s => s.GetDecisionForRequest(It.IsAny())).ReturnsAsync(new XacmlJsonResponse()); + AuthorizationClient client = new AuthorizationClient(Options.Create(new PlatformSettings()), httpContextAccessorMock.Object, httpClientMock.Object, appSettingsMock.Object, pdpMock.Object, NullLogger.Instance); + + var claimsPrincipal = GetClaims("1337"); + + var instance = new Instance() + { + Id = "1337/1dd16477-187b-463c-8adf-592c7fa78459", + Org = "tdd", + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + AppId = "tdd/test-app", + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + AltinnTaskType = "Data", + ElementId = "Task_1" + }, + EndEvent = "EndEvent_1" + } + }; + + var expected = new Dictionary(); + var actions = new List() + { + "read", + "write", + "complete", + "lookup" + }; + var actual = await client.AuthorizeActions(instance, claimsPrincipal, actions); + actual.Should().BeEquivalentTo(expected); + } + + private static ClaimsPrincipal GetClaims(string partyId) + { + return new ClaimsPrincipal(new List() + { + new(new List + { + new("urn:altinn:partyid", partyId, "#integer"), + new("urn:altinn:authlevel", "3", "#integer"), + }) + }); + } + + private static XacmlJsonResponse GetXacmlJsonRespons(string filename) + { + var xacmlJesonRespons = File.ReadAllText(Path.Join("Infrastructure", "Clients", "Authorization", "TestData", $"{filename}.json")); + return JsonSerializer.Deserialize(xacmlJesonRespons); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/TestData/one-action-denied.json b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/TestData/one-action-denied.json new file mode 100644 index 000000000..6994b5e3f --- /dev/null +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/TestData/one-action-denied.json @@ -0,0 +1,190 @@ +{ + "Response": [ + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "read", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "write", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "complete", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "NotApplicable", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": null, + "AssociateAdvice": null, + "Category": null, + "PolicyIdentifierList": null + } + ] +} diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs index 6add019e6..d281bcb58 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs @@ -12,11 +12,19 @@ using Moq; using Moq.Protected; using Xunit; +using Xunit.Abstractions; namespace Altinn.App.PlatformServices.Tests.Infrastructure.Clients { public class EventsSubscriptionClientTests { + private readonly ITestOutputHelper _testOutputHelper; + + public EventsSubscriptionClientTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + [Fact] public async Task AddSubscription_ShouldReturnOk() { diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/KeyVault/SecretsLocalClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/KeyVault/SecretsLocalClientTests.cs new file mode 100644 index 000000000..27351b949 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/KeyVault/SecretsLocalClientTests.cs @@ -0,0 +1,63 @@ +namespace Altinn.App.Core.Tests.Infrastructure.Clients.KeyVault; + +using System.Text.Json; +using Altinn.App.Core.Infrastructure.Clients.KeyVault; + +using FluentAssertions; +using Microsoft.Azure.KeyVault.WebKey; +using Microsoft.Extensions.Configuration; +using Xunit; + +public class SecretsLocalClientTests +{ + public static IConfiguration GetConfiguration(params (string Key, string Value)[] keys) + => new ConfigurationBuilder() + .AddInMemoryCollection(keys.ToDictionary(k => k.Key, k => k.Value)) + .Build(); + + [Fact] + public async Task TestMissingSecretId_ThrowsException() + { + var sut = new SecretsLocalClient(GetConfiguration(("test", "value"), ("d", "e"))); + + await sut.Invoking(s => s.GetCertificateAsync("certId")).Should().ThrowAsync(); + await sut.Invoking(s => s.GetKeyAsync("certId")).Should().ThrowAsync(); + await sut.Invoking(s => s.GetSecretAsync("certId")).Should().ThrowAsync(); + } + + [Fact] + public async Task TestCertificateFoundInConfiguration() + { + var certificate = new byte[20]; + Random.Shared.NextBytes(certificate); // Initialize with a randmo value + + var sut = new SecretsLocalClient(GetConfiguration(("certId", Convert.ToBase64String(certificate)), ("d", "e"))); + + var certResult = await sut.GetCertificateAsync("certId"); + certResult.Should().BeEquivalentTo(certificate); + } + + [Fact] + public async Task TestSecretFoundInSecretsJson() + { + var sut = new SecretsLocalClient(GetConfiguration()); + + var secretResult = await sut.GetSecretAsync("secretId"); + secretResult.Should().Be("local secret dummy data"); + } + + [Fact] + public async Task TestKeyFoundInSecretsJson() + { + var jwk = new JsonWebKey() + { + CurveName = "sillyCurveForTest" + }; + var jwkSerialized = JsonSerializer.Serialize(jwk); + var sut = new SecretsLocalClient(GetConfiguration(("jwk", jwkSerialized))); + + var keyResult = await sut.GetKeyAsync("jwk"); + keyResult.Should().BeEquivalentTo(jwk); + keyResult.CurveName.Should().Be("sillyCurveForTest"); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/DataClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs similarity index 78% rename from test/Altinn.App.Core.Tests/Infrastructure/Clients/DataClientTests.cs rename to test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs index 6eae354da..1728f091c 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/DataClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs @@ -6,9 +6,9 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; using Altinn.App.Core.Infrastructure.Clients.Storage; -using Altinn.App.Core.Infrastructure.Clients.Storage.TestData; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Models; +using Altinn.App.Core.Tests.Infrastructure.Clients.Storage.TestData; using Altinn.App.PlatformServices.Tests.Data; using Altinn.App.PlatformServices.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; @@ -19,7 +19,7 @@ using Moq; using Xunit; -namespace Altinn.App.Core.Tests.Infrastructure.Clients +namespace Altinn.App.Core.Tests.Infrastructure.Clients.Storage { public class DataClientTests { @@ -71,6 +71,39 @@ public async Task InsertBinaryData_MethodProduceValidPlatformRequest() Assert.NotNull(platformRequest); AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Post, "\"a cats story.pdf\"", "application/pdf"); } + + [Fact] + public async Task InsertBinaryData_MethodProduceValidPlatformRequest_with_generatedFrom_query_params() + { + // Arrange + HttpRequestMessage? platformRequest = null; + + var target = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + platformRequest = request; + + DataElement dataElement = new DataElement + { + Id = "DataElement.Id", + InstanceGuid = "InstanceGuid" + }; + await Task.CompletedTask; + return new HttpResponseMessage() { Content = JsonContent.Create(dataElement) }; + }); + + var stream = new MemoryStream(Encoding.UTF8.GetBytes("This is not a pdf, but no one here will care.")); + var instanceIdentifier = new InstanceIdentifier(323413, Guid.NewGuid()); + Uri expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data?dataType=catstories&generatedFromTask=Task_1", UriKind.RelativeOrAbsolute); + + // Act + DataElement actual = await target.InsertBinaryData(instanceIdentifier.ToString(), "catstories", "application/pdf", "a cats story.pdf", stream, "Task_1"); + + // Assert + Assert.NotNull(actual); + + Assert.NotNull(platformRequest); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Post, "\"a cats story.pdf\"", "application/pdf"); + } [Fact] public async Task GetFormData_MethodProduceValidPlatformRequest_ReturnedFormIsValid() @@ -334,7 +367,7 @@ public async Task GetBinaryDataList_returns_AttachemtList_when_DataElements_foun }; response.Should().BeEquivalentTo(expectedList); } - + [Fact] public async Task GetBinaryDataList_throws_PlatformHttpException_if_non_ok_response() { @@ -352,7 +385,7 @@ public async Task GetBinaryDataList_throws_PlatformHttpException_if_non_ok_respo actual.Should().NotBeNull(); actual.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); } - + [Fact] public async Task DeleteBinaryData_returns_true_when_data_was_deleted() { @@ -375,7 +408,7 @@ public async Task DeleteBinaryData_returns_true_when_data_was_deleted() AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); result.Should().BeTrue(); } - + [Fact] public async Task DeleteBinaryData_throws_PlatformHttpException_when_dataelement_not_found() { @@ -397,7 +430,7 @@ public async Task DeleteBinaryData_throws_PlatformHttpException_when_dataelement AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); actual.Response.StatusCode.Should().Be(HttpStatusCode.NotFound); } - + [Fact] public async Task DeleteData_returns_true_when_data_was_deleted_with_delay_true() { @@ -446,7 +479,7 @@ public async Task UpdateData_serializes_and_updates_formdata() platformRequest?.Should().NotBeNull(); AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put, null, "application/xml"); } - + [Fact] public async Task UpdateData_throws_error_if_serilization_fails() { @@ -467,7 +500,7 @@ public async Task UpdateData_throws_error_if_serilization_fails() await Assert.ThrowsAsync(async () => await dataClient.UpdateData(exampleModel, instanceIdentifier.InstanceGuid, typeof(DataElement), "ttd", "app", instanceIdentifier.InstanceOwnerPartyId, dataGuid)); invocations.Should().Be(0); } - + [Fact] public async Task UpdateData_throws_platformhttpexception_if_platform_request_fails() { @@ -488,13 +521,110 @@ public async Task UpdateData_throws_platformhttpexception_if_platform_request_fa return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; }); var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", UriKind.RelativeOrAbsolute); - var result = await Assert.ThrowsAsync(async () => await dataClient.UpdateData(exampleModel, instanceIdentifier.InstanceGuid, typeof(ExampleModel), "ttd", "app", instanceIdentifier.InstanceOwnerPartyId, dataGuid)); + var result = await Assert.ThrowsAsync(async () => + await dataClient.UpdateData(exampleModel, instanceIdentifier.InstanceGuid, typeof(ExampleModel), "ttd", "app", instanceIdentifier.InstanceOwnerPartyId, dataGuid)); invocations.Should().Be(1); platformRequest?.Should().NotBeNull(); AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put, null, "application/xml"); result.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); } + [Fact] + public async Task LockDataElement_calls_lock_endpoint_in_storage_and_returns_updated_DataElement() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + DataElement dataElement = new() + { + Id = "67a5ef12-6e38-41f8-8b42-f91249ebcec0", + Locked = true + }; + var dataClient = GetDataClient(async (request, token) => + { + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, Content = new StringContent("{\"id\":\"67a5ef12-6e38-41f8-8b42-f91249ebcec0\",\"locked\":true}") }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", UriKind.RelativeOrAbsolute); + var response = await dataClient.LockDataElement(instanceIdentifier, dataGuid); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + response.Should().BeEquivalentTo(dataElement); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put); + } + + [Fact] + public async Task LockDataElement_throws_platformhttpexception_if_platform_request_fails() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + int invocations = 0; + HttpRequestMessage? platformRequest = null; + var dataClient = GetDataClient(async (request, token) => + { + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", UriKind.RelativeOrAbsolute); + var result = await Assert.ThrowsAsync(async () => await dataClient.LockDataElement(instanceIdentifier, dataGuid)); + invocations.Should().Be(1); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put); + result.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } + + [Fact] + public async Task UnlockDataElement_calls_lock_endpoint_in_storage_and_returns_updated_DataElement() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + DataElement dataElement = new() + { + Id = "67a5ef12-6e38-41f8-8b42-f91249ebcec0", + Locked = true + }; + var dataClient = GetDataClient(async (request, token) => + { + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, Content = new StringContent("{\"id\":\"67a5ef12-6e38-41f8-8b42-f91249ebcec0\",\"locked\":true}") }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", UriKind.RelativeOrAbsolute); + var response = await dataClient.UnlockDataElement(instanceIdentifier, dataGuid); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + response.Should().BeEquivalentTo(dataElement); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); + } + + [Fact] + public async Task UnlockDataElement_throws_platformhttpexception_if_platform_request_fails() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + int invocations = 0; + HttpRequestMessage? platformRequest = null; + var dataClient = GetDataClient(async (request, token) => + { + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", UriKind.RelativeOrAbsolute); + var result = await Assert.ThrowsAsync(async () => await dataClient.UnlockDataElement(instanceIdentifier, dataGuid)); + invocations.Should().Be(1); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); + result.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } + private DataClient GetDataClient(Func> handlerFunc) { DelegatingHandlerStub delegatingHandlerStub = new(handlerFunc); @@ -513,6 +643,7 @@ private void AssertHttpRequest(HttpRequestMessage actual, Uri expectedUri, HttpM actual.Content?.Headers.TryGetValues("Content-Disposition", out actualContentDisposition); var authHeader = actual.Headers.Authorization; actual.RequestUri.Should().BeEquivalentTo(expectedUri); + actual.Method.Should().BeEquivalentTo(method); Uri.Compare(actual.RequestUri, expectedUri, UriComponents.HttpRequestUrl, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase).Should().Be(0, "Actual request Uri did not match expected Uri"); if (expectedContentType is not null) { diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs new file mode 100644 index 000000000..b886f645d --- /dev/null +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs @@ -0,0 +1,313 @@ +using System.Net; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Infrastructure.Clients.Storage; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Tests.TestHelpers; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Primitives; +using Moq; +using Prometheus; +using Xunit; + +namespace Altinn.App.Core.Tests.Infrastructure.Clients.Storage; + +public class InstanceClientMetricsDecoratorTests +{ + public InstanceClientMetricsDecoratorTests() + { + Metrics.SuppressDefaultMetrics(); + } + + [Fact] + public async Task CreateInstance_calls_decorated_service_and_update_on_success() + { + // Arrange + Mock instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + var instanceTemplate = new Instance(); + + // Act + await instanceClientMetricsDecorator.CreateInstance("org", "app", instanceTemplate); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().HaveCountGreaterOrEqualTo(1); + diff.Should().Contain("altinn_app_instances_created{result=\"success\"} 1"); + instanceClient.Verify(i => i.CreateInstance(It.IsAny(), It.IsAny(), It.IsAny())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task CreateInstance_calls_decorated_service_and_update_on_failure() + { + // Arrange + var instanceClient = new Mock(); + var platformHttpException = new PlatformHttpException(new HttpResponseMessage(HttpStatusCode.BadRequest), "test"); + instanceClient.Setup(i => i.CreateInstance(It.IsAny(), It.IsAny(), It.IsAny())).ThrowsAsync(platformHttpException); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + var instanceTemplate = new Instance(); + + // Act + var ex = await Assert.ThrowsAsync(async () => await instanceClientMetricsDecorator.CreateInstance("org", "app", instanceTemplate)); + ex.Should().BeSameAs(platformHttpException); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().HaveCountGreaterOrEqualTo(1); + diff.Should().Contain("altinn_app_instances_created{result=\"failure\"} 1"); + instanceClient.Verify(i => i.CreateInstance(It.IsAny(), It.IsAny(), It.IsAny())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task AddCompleteConfirmation_calls_decorated_service_and_update_on_success() + { + // Arrange + Mock instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Act + await instanceClientMetricsDecorator.AddCompleteConfirmation(1337, Guid.NewGuid()); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().HaveCountGreaterOrEqualTo(1); + diff.Should().Contain("altinn_app_instances_completed{result=\"success\"} 1"); + instanceClient.Verify(i => i.AddCompleteConfirmation(It.IsAny(), It.IsAny())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task AddCompleteConfirmation_calls_decorated_service_and_update_on_failure() + { + // Arrange + var instanceClient = new Mock(); + var platformHttpException = new PlatformHttpException(new HttpResponseMessage(HttpStatusCode.BadRequest), "test"); + instanceClient.Setup(i => i.AddCompleteConfirmation(It.IsAny(), It.IsAny())).ThrowsAsync(platformHttpException); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Act + var ex = await Assert.ThrowsAsync(async () => await instanceClientMetricsDecorator.AddCompleteConfirmation(1337, Guid.NewGuid())); + ex.Should().BeSameAs(platformHttpException); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().HaveCountGreaterOrEqualTo(1); + diff.Should().Contain("altinn_app_instances_completed{result=\"failure\"} 1"); + instanceClient.Verify(i => i.AddCompleteConfirmation(It.IsAny(), It.IsAny())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task DeleteInstance_calls_decorated_service_and_update_on_success_soft_delete() + { + // Arrange + Mock instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Act + await instanceClientMetricsDecorator.DeleteInstance(1337, Guid.NewGuid(), false); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().HaveCountGreaterOrEqualTo(1); + diff.Should().Contain("altinn_app_instances_deleted{result=\"success\",mode=\"soft\"} 1"); + instanceClient.Verify(i => i.DeleteInstance(It.IsAny(), It.IsAny(), It.IsAny())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task DeleteInstance_calls_decorated_service_and_update_on_success_soft_hard() + { + // Arrange + Mock instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Act + await instanceClientMetricsDecorator.DeleteInstance(1337, Guid.NewGuid(), true); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().HaveCountGreaterOrEqualTo(1); + diff.Should().Contain("altinn_app_instances_deleted{result=\"success\",mode=\"hard\"} 1"); + instanceClient.Verify(i => i.DeleteInstance(It.IsAny(), It.IsAny(), It.IsAny())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task DeleteInstance_calls_decorated_service_and_update_on_failure() + { + // Arrange + var instanceClient = new Mock(); + var platformHttpException = new PlatformHttpException(new HttpResponseMessage(HttpStatusCode.BadRequest), "test"); + instanceClient.Setup(i => i.DeleteInstance(It.IsAny(), It.IsAny(), It.IsAny())).ThrowsAsync(platformHttpException); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Act + var ex = await Assert.ThrowsAsync(async () => await instanceClientMetricsDecorator.DeleteInstance(1337, Guid.NewGuid(), false)); + ex.Should().BeSameAs(platformHttpException); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().HaveCountGreaterOrEqualTo(1); + diff.Should().Contain("altinn_app_instances_deleted{result=\"failure\",mode=\"soft\"} 1"); + instanceClient.Verify(i => i.DeleteInstance(It.IsAny(), It.IsAny(), It.IsAny())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task GetInstance_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + + // Act + var instanceId = Guid.NewGuid(); + await instanceClientMetricsDecorator.GetInstance("test-app", "ttd", 1337, instanceId); + + // Assert + instanceClient.Verify(i => i.GetInstance("test-app", "ttd", 1337, instanceId)); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task GetInstance_instance_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var instance = new Instance(); + + // Act + await instanceClientMetricsDecorator.GetInstance(instance); + + // Assert + instanceClient.Verify(i => i.GetInstance(instance)); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task GetInstances_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + + // Act + await instanceClientMetricsDecorator.GetInstances(new Dictionary()); + + // Assert + instanceClient.Verify(i => i.GetInstances(new Dictionary())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateProcess_of_instance_owner_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var instance = new Instance(); + + // Act + await instanceClientMetricsDecorator.UpdateProcess(instance); + + // Assert + instanceClient.Verify(i => i.UpdateProcess(instance)); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateReadStatus_of_instance_owner_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var instanceGuid = Guid.NewGuid(); + + // Act + await instanceClientMetricsDecorator.UpdateReadStatus(1337, instanceGuid, "read"); + + // Assert + instanceClient.Verify(i => i.UpdateReadStatus(1337, instanceGuid, "read")); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateSubstatus_of_instance_owner_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var instanceGuid = Guid.NewGuid(); + var substatus = new Substatus(); + + // Act + await instanceClientMetricsDecorator.UpdateSubstatus(1337, instanceGuid, substatus); + + // Assert + instanceClient.Verify(i => i.UpdateSubstatus(1337, instanceGuid, substatus)); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdatePresentationTexts_of_instance_owner_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var instanceGuid = Guid.NewGuid(); + var presentationTexts = new PresentationTexts(); + + // Act + await instanceClientMetricsDecorator.UpdatePresentationTexts(1337, instanceGuid, presentationTexts); + + // Assert + instanceClient.Verify(i => i.UpdatePresentationTexts(1337, instanceGuid, presentationTexts)); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateDataValues_of_instance_owner_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var instanceGuid = Guid.NewGuid(); + var dataValues = new DataValues(); + + // Act + await instanceClientMetricsDecorator.UpdateDataValues(1337, instanceGuid, dataValues); + + // Assert + instanceClient.Verify(i => i.UpdateDataValues(1337, instanceGuid, dataValues)); + instanceClient.VerifyNoOtherCalls(); + } + + private static List GetDiff(string s1, string s2) + { + List diff; + IEnumerable set1 = s1.Split('\n').Distinct().Where(s => !s.StartsWith("#")); + IEnumerable set2 = s2.Split('\n').Distinct().Where(s => !s.StartsWith("#")); + + diff = set2.Count() > set1.Count() ? set2.Except(set1).ToList() : set1.Except(set2).ToList(); + + return diff; + } +} diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs new file mode 100644 index 000000000..a16408a41 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs @@ -0,0 +1,139 @@ +#nullable enable +using System.Net; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Infrastructure.Clients.Storage; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Sign; +using Altinn.App.Core.Models; +using Altinn.App.PlatformServices.Tests.Mocks; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; +using Signee = Altinn.App.Core.Internal.Sign.Signee; + +namespace Altinn.App.Core.Tests.Infrastructure.Clients.Storage; + +public class SignClientTests +{ + private readonly IOptions platformSettingsOptions; + private readonly Mock userTokenProvide; + private readonly string apiStorageEndpoint = "https://local.platform.altinn.no/api/storage/"; + + public SignClientTests() + { + platformSettingsOptions = Options.Create(new PlatformSettings() + { + ApiStorageEndpoint = apiStorageEndpoint, + SubscriptionKey = "test" + }); + + userTokenProvide = new Mock(); + userTokenProvide.Setup(s => s.GetUserToken()).Returns("dummytoken"); + } + + [Fact] + public async Task SignDataElements_sends_request_to_platform() + { + // Arrange + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(1337, Guid.NewGuid()); + HttpRequestMessage? platformRequest = null; + int callCount = 0; + SignClient signClient = GetSignClient((request, token) => + { + callCount++; + platformRequest = request; + return Task.FromResult(new HttpResponseMessage + { + StatusCode = HttpStatusCode.Created + }); + }); + + // Act + var dataElementId = Guid.NewGuid().ToString(); + var signatureContext = new SignatureContext( + instanceIdentifier, + "sign-data-type", + new Signee() + { + UserId = "1337", + PersonNumber = "0101011337" + }, + new DataElementSignature(dataElementId)); + + SignRequest expectedRequest = new SignRequest() + { + Signee = new() + { + UserId = "1337", + PersonNumber = "0101011337" + }, + DataElementSignatures = new() + { + new() + { + DataElementId = dataElementId, + Signed = true + } + }, + SignatureDocumentDataType = "sign-data-type" + }; + + await signClient.SignDataElements(signatureContext); + + // Assert + userTokenProvide.Verify(s => s.GetUserToken(), Times.Once); + callCount.Should().Be(1); + platformRequest.Should().NotBeNull(); + platformRequest!.Method.Should().Be(HttpMethod.Post); + platformRequest!.RequestUri!.ToString().Should().Be($"{apiStorageEndpoint}instances/{instanceIdentifier.InstanceOwnerPartyId}/{instanceIdentifier.InstanceGuid}/sign"); + SignRequest actual = await JsonSerializerPermissive.DeserializeAsync(platformRequest!.Content!); + actual.Should().BeEquivalentTo(expectedRequest); + } + + [Fact] + public async Task SignDataElements_throws_PlatformHttpException_if_platform_returns_http_errorcode() + { + // Arrange + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(1337, Guid.NewGuid()); + HttpRequestMessage? platformRequest = null; + int callCount = 0; + SignClient signClient = GetSignClient((request, token) => + { + callCount++; + platformRequest = request; + return Task.FromResult(new HttpResponseMessage + { + StatusCode = HttpStatusCode.InternalServerError + }); + }); + + // Act + var dataElementId = Guid.NewGuid().ToString(); + var signatureContext = new SignatureContext( + instanceIdentifier, + "sign-data-type", + new Signee() + { + UserId = "1337", + PersonNumber = "0101011337" + }, + new DataElementSignature(dataElementId)); + + var ex = await Assert.ThrowsAsync(async() => await signClient.SignDataElements(signatureContext)); + ex.Should().NotBeNull(); + ex.Response.Should().NotBeNull(); + ex.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } + + private SignClient GetSignClient(Func> handlerFunc) + { + DelegatingHandlerStub delegatingHandlerStub = new(handlerFunc); + return new SignClient( + platformSettingsOptions, + new HttpClient(delegatingHandlerStub), + userTokenProvide.Object); + } +} diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/TestData/ExampleModel.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/TestData/ExampleModel.cs similarity index 64% rename from src/Altinn.App.Core/Infrastructure/Clients/Storage/TestData/ExampleModel.cs rename to test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/TestData/ExampleModel.cs index 7643dbf51..5f22ebefb 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/TestData/ExampleModel.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/TestData/ExampleModel.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Infrastructure.Clients.Storage.TestData; +namespace Altinn.App.Core.Tests.Infrastructure.Clients.Storage.TestData; /// /// Example Model used in tests @@ -8,7 +8,8 @@ public class ExampleModel /// /// The name /// - public string Name { get; set; } = ""; + public string Name { get; set; } = string.Empty; + /// /// The age /// diff --git a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs index 140da41e2..84675dd8f 100644 --- a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs @@ -1,3 +1,5 @@ +using System.Reflection; +using System.Text.Json; using Altinn.App.Core.Configuration; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; @@ -6,7 +8,9 @@ using Microsoft.Extensions.Options; using Microsoft.FeatureManagement; using Moq; +using Newtonsoft.Json; using Xunit; +using JsonSerializer = System.Text.Json.JsonSerializer; namespace Altinn.App.Core.Tests.Internal.App { @@ -461,6 +465,30 @@ public async Task GetApplicationMetadata_logo_can_intstantiate_with_source_and_D actual.Should().BeEquivalentTo(expected); } + [Fact] + public async Task GetApplicationMetadata_deserializes_unmapped_properties() + { + AppSettings appSettings = GetAppSettings("AppMetadata", "unmapped-properties.applicationmetadata.json"); + IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings)); + var actual = await appMetadata.GetApplicationMetadata(); + actual.Should().NotBeNull(); + actual.UnmappedProperties.Should().NotBeNull(); + actual.UnmappedProperties!["foo"].Should().BeOfType(); + ((JsonElement)actual.UnmappedProperties["foo"]).GetProperty("bar").GetString().Should().Be("baz"); + } + + [Fact] + public async Task GetApplicationMetadata_deserialize_serialize_unmapped_properties() + { + AppSettings appSettings = GetAppSettings("AppMetadata", "unmapped-properties.applicationmetadata.json"); + IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings)); + var appMetadataObj = await appMetadata.GetApplicationMetadata(); + string serialized = JsonSerializer.Serialize(appMetadataObj, new JsonSerializerOptions { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + string expected = File.ReadAllText(Path.Join(appBasePath, "AppMetadata", "unmapped-properties.applicationmetadata.expected.json")); + expected = expected.Replace("--AltinnNugetVersion--", typeof(ApplicationMetadata).Assembly!.GetName().Version!.ToString()); + serialized.Should().Be(expected); + } + [Fact] public async void GetApplicationMetadata_throws_ApplicationConfigException_if_file_not_found() { diff --git a/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs b/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs index 15e1350c8..1efb76060 100644 --- a/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs @@ -12,22 +12,34 @@ public class FrontendFeaturesTest [Fact] public async Task GetFeatures_returns_list_of_enabled_features() { + Dictionary expected = new Dictionary() + { + { "footer", true }, + { "processActions", true }, + { "jsonObjectInDataResponse", false }, + }; var featureManagerMock = new Mock(); IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); var actual = await frontendFeatures.GetFrontendFeatures(); - actual.Should().Contain(new KeyValuePair("footer", true)); + actual.Should().BeEquivalentTo(expected); } [Fact] public async Task GetFeatures_returns_list_of_enabled_features_when_feature_flag_is_enabled() { + Dictionary expected = new Dictionary() + { + { "footer", true }, + { "processActions", true }, + { "jsonObjectInDataResponse", true }, + }; var featureManagerMock = new Mock(); featureManagerMock.Setup(f => f.IsEnabledAsync(FeatureFlags.JsonObjectInDataResponse)).ReturnsAsync(true); IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); var actual = await frontendFeatures.GetFrontendFeatures(); - actual.Should().Contain(new KeyValuePair("jsonObjectInDataResponse", true)); + actual.Should().BeEquivalentTo(expected); } } } diff --git a/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/unmapped-properties.applicationmetadata.expected.json b/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/unmapped-properties.applicationmetadata.expected.json new file mode 100644 index 000000000..88b4be5b3 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/unmapped-properties.applicationmetadata.expected.json @@ -0,0 +1,83 @@ +{ + "Id": "tdd/bestilling", + "Features": { + "footer": true, + "processActions": true, + "jsonObjectInDataResponse": false + }, + "OnEntry": { + "InstanceSelection": null, + "Show": "select-instance" + }, + "Logo": null, + "AltinnNugetVersion": "--AltinnNugetVersion--", + "VersionId": null, + "Org": "tdd", + "Title": { + "nb": "Bestillingseksempelapp" + }, + "ValidFrom": null, + "ValidTo": null, + "ProcessId": null, + "DataTypes": [ + { + "Id": "vedlegg", + "Description": null, + "AllowedContentTypes": [ + "application/pdf", + "image/png", + "image/jpeg" + ], + "AllowedContributers": null, + "AppLogic": null, + "TaskId": "Task_1", + "MaxSize": null, + "MaxCount": 0, + "MinCount": 0, + "Grouping": null, + "EnablePdfCreation": true, + "EnableFileScan": false, + "ValidationErrorOnPendingFileScan": false, + "EnabledFileAnalysers": [], + "EnabledFileValidators": [] + }, + { + "Id": "ref-data-as-pdf", + "Description": null, + "AllowedContentTypes": [ + "application/pdf" + ], + "AllowedContributers": null, + "AppLogic": null, + "TaskId": "Task_1", + "MaxSize": null, + "MaxCount": 0, + "MinCount": 1, + "Grouping": null, + "EnablePdfCreation": true, + "EnableFileScan": false, + "ValidationErrorOnPendingFileScan": false, + "EnabledFileAnalysers": [], + "EnabledFileValidators": [] + } + ], + "PartyTypesAllowed": { + "BankruptcyEstate": true, + "Organisation": true, + "Person": true, + "SubUnit": true + }, + "AutoDeleteOnProcessEnd": false, + "PresentationFields": null, + "DataFields": null, + "EFormidling": null, + "MessageBoxConfig": null, + "CopyInstanceSettings": null, + "Created": "2019-09-16T22:22:22", + "CreatedBy": "username", + "LastChanged": null, + "LastChangedBy": null, + "foo": { + "bar": "baz" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/unmapped-properties.applicationmetadata.json b/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/unmapped-properties.applicationmetadata.json new file mode 100644 index 000000000..f0e1b5d65 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/unmapped-properties.applicationmetadata.json @@ -0,0 +1,33 @@ +{ + "id": "tdd/bestilling", + "org": "tdd", + "created": "2019-09-16T22:22:22", + "createdBy": "username", + "title": { "nb": "Bestillingseksempelapp" }, + "dataTypes": [ + { + "id": "vedlegg", + "allowedContentTypes": [ "application/pdf", "image/png", "image/jpeg" ], + "minCount": 0, + "taskId": "Task_1" + }, + { + "id": "ref-data-as-pdf", + "allowedContentTypes": [ "application/pdf" ], + "minCount": 1, + "taskId": "Task_1" + } + ], + "partyTypesAllowed": { + "bankruptcyEstate": true, + "organisation": true, + "person": true, + "subUnit": true + }, + "onEntry": { + "show": "select-instance" + }, + "foo": { + "bar": "baz" + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs new file mode 100644 index 000000000..26f21b441 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs @@ -0,0 +1,338 @@ +#nullable enable +using System.Security.Claims; +using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Process.Action; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Models; +using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Auth; + +public class AuthorizationServiceTests +{ + [Fact] + public async Task GetPartyList_returns_party_list_from_AuthorizationClient() + { + // Input + int userId = 1337; + + // Arrange + Mock authorizationClientMock = new Mock(); + List partyList = new List(); + authorizationClientMock.Setup(a => a.GetPartyList(userId)).ReturnsAsync(partyList); + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List()); + + // Act + List? result = await authorizationService.GetPartyList(userId); + + // Assert + result.Should().BeSameAs(partyList); + authorizationClientMock.Verify(a => a.GetPartyList(userId), Times.Once); + } + + [Fact] + public async Task ValidateSelectedParty_returns_validation_from_AuthorizationClient() + { + // Input + int userId = 1337; + int partyId = 1338; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.ValidateSelectedParty(userId, partyId)).ReturnsAsync(true); + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List()); + + // Act + bool? result = await authorizationService.ValidateSelectedParty(userId, partyId); + + // Assert + result.Should().BeTrue(); + authorizationClientMock.Verify(a => a.ValidateSelectedParty(userId, partyId), Times.Once); + } + + [Fact] + public async Task AuthorizeAction_returns_true_when_AutorizationClient_true_and_no_IUserActinAuthorizerProvider_is_provided() + { + // Input + AppIdentifier appIdentifier = new AppIdentifier("ttd/xunit-app"); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId: 1337, instanceGuid: Guid.NewGuid()); + ClaimsPrincipal user = new ClaimsPrincipal(); + string action = "action"; + string taskId = "taskId"; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(true); + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List()); + + // Act + bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); + + // Assert + result.Should().BeTrue(); + authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); + } + + [Fact] + public async Task AuthorizeAction_returns_false_when_AutorizationClient_false_and_no_IUserActinAuthorizerProvider_is_provided() + { + // Input + AppIdentifier appIdentifier = new AppIdentifier("ttd/xunit-app"); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId: 1337, instanceGuid: Guid.NewGuid()); + ClaimsPrincipal user = new ClaimsPrincipal(); + string action = "action"; + string taskId = "taskId"; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(false); + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List()); + + // Act + bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); + + // Assert + result.Should().BeFalse(); + authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); + } + + [Fact] + public async Task AuthorizeAction_returns_false_when_AutorizationClient_true_and_one_IUserActinAuthorizerProvider_returns_false() + { + // Input + AppIdentifier appIdentifier = new AppIdentifier("ttd/xunit-app"); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId: 1337, instanceGuid: Guid.NewGuid()); + ClaimsPrincipal user = new ClaimsPrincipal(); + string action = "action"; + string taskId = "taskId"; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(true); + + Mock userActionAuthorizerMock = new Mock(); + userActionAuthorizerMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(false); + IUserActionAuthorizerProvider userActionAuthorizerProvider = new UserActionAuthorizerProvider("taskId", "action", userActionAuthorizerMock.Object); + + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List() { userActionAuthorizerProvider }); + + // Act + bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); + + // Assert + result.Should().BeFalse(); + authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); + userActionAuthorizerMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); + } + + [Fact] + public async Task AuthorizeAction_does_not_call_UserActionAuthorizer_if_AuthorizationClient_returns_false() + { + // Input + AppIdentifier appIdentifier = new AppIdentifier("ttd/xunit-app"); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId: 1337, instanceGuid: Guid.NewGuid()); + ClaimsPrincipal user = new ClaimsPrincipal(); + string action = "action"; + string taskId = "taskId"; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(false); + + Mock userActionAuthorizerMock = new Mock(); + userActionAuthorizerMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); + IUserActionAuthorizerProvider userActionAuthorizerProvider = new UserActionAuthorizerProvider("taskId", "action", userActionAuthorizerMock.Object); + + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List() { userActionAuthorizerProvider }); + + // Act + bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); + + // Assert + result.Should().BeFalse(); + authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); + userActionAuthorizerMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Never); + } + + [Fact] + public async Task AuthorizeAction_calls_all_providers_and_return_true_if_all_true() + { + // Input + AppIdentifier appIdentifier = new AppIdentifier("ttd/xunit-app"); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId: 1337, instanceGuid: Guid.NewGuid()); + ClaimsPrincipal user = new ClaimsPrincipal(); + string action = "action"; + string taskId = "taskId"; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(true); + + Mock userActionAuthorizerOneMock = new Mock(); + userActionAuthorizerOneMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); + IUserActionAuthorizerProvider userActionAuthorizerOneProvider = new UserActionAuthorizerProvider("taskId", "action", userActionAuthorizerOneMock.Object); + Mock userActionAuthorizerTwoMock = new Mock(); + userActionAuthorizerTwoMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); + IUserActionAuthorizerProvider userActionAuthorizerTwoProvider = new UserActionAuthorizerProvider("taskId", "action", userActionAuthorizerTwoMock.Object); + + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List() { userActionAuthorizerOneProvider, userActionAuthorizerTwoProvider }); + + // Act + bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); + + // Assert + result.Should().BeTrue(); + authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); + userActionAuthorizerOneMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); + userActionAuthorizerTwoMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); + } + + [Fact] + public async Task AuthorizeAction_does_not_call_providers_with_non_matching_taskId_and_or_action() + { + // Input + AppIdentifier appIdentifier = new AppIdentifier("ttd/xunit-app"); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId: 1337, instanceGuid: Guid.NewGuid()); + ClaimsPrincipal user = new ClaimsPrincipal(); + string action = "action"; + string taskId = "taskId"; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(true); + + Mock userActionAuthorizerOneMock = new Mock(); + userActionAuthorizerOneMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(false); + IUserActionAuthorizerProvider userActionAuthorizerOneProvider = new UserActionAuthorizerProvider("taskId", "action2", userActionAuthorizerOneMock.Object); + + Mock userActionAuthorizerTwoMock = new Mock(); + userActionAuthorizerTwoMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(false); + IUserActionAuthorizerProvider userActionAuthorizerTwoProvider = new UserActionAuthorizerProvider("taskId2", "action", userActionAuthorizerTwoMock.Object); + + Mock userActionAuthorizerThreeMock = new Mock(); + userActionAuthorizerThreeMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(false); + IUserActionAuthorizerProvider userActionAuthorizerThreeProvider = new UserActionAuthorizerProvider("taskId3", "action3", userActionAuthorizerThreeMock.Object); + + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List() { userActionAuthorizerOneProvider, userActionAuthorizerTwoProvider, userActionAuthorizerThreeProvider }); + + // Act + bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); + + // Assert + result.Should().BeTrue(); + authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); + userActionAuthorizerOneMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Never); + userActionAuthorizerTwoMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Never); + userActionAuthorizerThreeMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Never); + } + + [Fact] + public async Task AuthorizeAction_calls_providers_with_task_null_and_or_action_null() + { + // Input + AppIdentifier appIdentifier = new AppIdentifier("ttd/xunit-app"); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId: 1337, instanceGuid: Guid.NewGuid()); + ClaimsPrincipal user = new ClaimsPrincipal(); + string action = "action"; + string taskId = "taskId"; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(true); + + Mock userActionAuthorizerOneMock = new Mock(); + userActionAuthorizerOneMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); + IUserActionAuthorizerProvider userActionAuthorizerOneProvider = new UserActionAuthorizerProvider(null, "action", userActionAuthorizerOneMock.Object); + + Mock userActionAuthorizerTwoMock = new Mock(); + userActionAuthorizerTwoMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); + IUserActionAuthorizerProvider userActionAuthorizerTwoProvider = new UserActionAuthorizerProvider("taskId", null, userActionAuthorizerTwoMock.Object); + + Mock userActionAuthorizerThreeMock = new Mock(); + userActionAuthorizerThreeMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); + IUserActionAuthorizerProvider userActionAuthorizerThreeProvider = new UserActionAuthorizerProvider(null, null, userActionAuthorizerThreeMock.Object); + + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List() { userActionAuthorizerOneProvider, userActionAuthorizerTwoProvider, userActionAuthorizerThreeProvider }); + + // Actπ + bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); + + // Assert + result.Should().BeTrue(); + authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); + userActionAuthorizerOneMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); + userActionAuthorizerTwoMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); + userActionAuthorizerThreeMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); + } + + [Fact] + private async Task AuthorizeActions_returns_list_of_UserActions_with_auth_decisions() + { + // Input + Instance instance = new Instance(); + ClaimsPrincipal user = new ClaimsPrincipal(); + List actions = new List() + { + new AltinnAction("read"), + new AltinnAction("write"), + new AltinnAction("brew-coffee"), + new AltinnAction("drink-coffee", ActionType.ServerAction), + }; + var actionsStrings = new List() { "read", "write", "brew-coffee", "drink-coffee" }; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeActions(instance, user, actionsStrings)).ReturnsAsync(new Dictionary() + { + { "read", true }, + { "write", true }, + { "brew-coffee", true }, + { "drink-coffee", false } + }); + + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List()); + + // Act + List result = await authorizationService.AuthorizeActions(instance, user, actions); + + List expected = new List() + { + new UserAction() + { + Id = "read", + ActionType = ActionType.ProcessAction, + Authorized = true + }, + new UserAction() + { + Id = "write", + ActionType = ActionType.ProcessAction, + Authorized = true + }, + new UserAction() + { + Id = "brew-coffee", + ActionType = ActionType.ProcessAction, + Authorized = true + }, + new UserAction() + { + Id = "drink-coffee", + ActionType = ActionType.ServerAction, + Authorized = false + } + }; + + // Assert + result.Should().BeEquivalentTo(expected); + authorizationClientMock.Verify(a => a.AuthorizeActions(instance, user, actionsStrings), Times.Once); + authorizationClientMock.VerifyNoOtherCalls(); + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs index f5b45b7b0..c4663464a 100644 --- a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs @@ -4,8 +4,12 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; using Altinn.App.Core.Infrastructure.Clients.Pdf; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Pdf; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.App.PlatformServices.Tests.Helpers; using Altinn.App.PlatformServices.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; @@ -24,11 +28,11 @@ public class PdfServiceTests private readonly Mock _pdf = new(); private readonly Mock _appResources = new(); private readonly Mock _pdfOptionsMapping = new(); - private readonly Mock _dataClient = new(); + private readonly Mock _dataClient = new(); private readonly Mock _httpContextAccessor = new(); private readonly Mock _pdfGeneratorClient = new(); - private readonly Mock _profile = new(); - private readonly Mock _register = new(); + private readonly Mock _profile = new(); + private readonly Mock _register = new(); private readonly Mock pdfFormatter = new(); private readonly IOptions _pdfGeneratorSettingsOptions = Microsoft.Extensions.Options.Options.Create(new() { }); @@ -117,16 +121,16 @@ public async Task GenerateAndStorePdf() }; // Act - await target.GenerateAndStorePdf(instance, CancellationToken.None); + await target.GenerateAndStorePdf(instance, "Task_1", CancellationToken.None); // Asserts _pdfGeneratorClient.Verify( s => s.GeneratePdf( It.Is( u => u.Scheme == "https" && - u.Host == $"{instance.Org}.apps.{HostName}" && - u.AbsoluteUri.Contains(instance.AppId) && - u.AbsoluteUri.Contains(instance.Id)), + u.Host == $"{instance.Org}.apps.{HostName}" && + u.AbsoluteUri.Contains(instance.AppId) && + u.AbsoluteUri.Contains(instance.Id)), It.IsAny()), Times.Once); @@ -136,7 +140,84 @@ public async Task GenerateAndStorePdf() It.Is(s => s == "ref-data-as-pdf"), It.Is(s => s == "application/pdf"), It.Is(s => s == "not-really-an-app.pdf"), - It.IsAny()), + It.IsAny(), + It.Is(s => s == "Task_1")), + Times.Once); + } + + [Fact] + public async Task GenerateAndStorePdf_with_generatedFrom() + { + // Arrange + _pdfGeneratorClient.Setup(s => s.GeneratePdf(It.IsAny(), It.IsAny())); + + _generalSettingsOptions.Value.ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}"; + + var target = new PdfService( + _pdf.Object, + _appResources.Object, + _pdfOptionsMapping.Object, + _dataClient.Object, + _httpContextAccessor.Object, + _profile.Object, + _register.Object, + pdfFormatter.Object, + _pdfGeneratorClient.Object, + _pdfGeneratorSettingsOptions, + _generalSettingsOptions); + + var dataModelId = Guid.NewGuid(); + var attachmentId = Guid.NewGuid(); + + Instance instance = new() + { + Id = $"509378/{Guid.NewGuid()}", + AppId = "digdir/not-really-an-app", + Org = "digdir", + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + }, + Data = new() + { + new() + { + Id = dataModelId.ToString(), + DataType = "Model" + }, + new() + { + Id = attachmentId.ToString(), + DataType = "attachment" + } + } + }; + + // Act + await target.GenerateAndStorePdf(instance, "Task_1", CancellationToken.None); + + // Asserts + _pdfGeneratorClient.Verify( + s => s.GeneratePdf( + It.Is( + u => u.Scheme == "https" && + u.Host == $"{instance.Org}.apps.{HostName}" && + u.AbsoluteUri.Contains(instance.AppId) && + u.AbsoluteUri.Contains(instance.Id)), + It.IsAny()), + Times.Once); + + _dataClient.Verify( + s => s.InsertBinaryData( + It.Is(s => s == instance.Id), + It.Is(s => s == "ref-data-as-pdf"), + It.Is(s => s == "application/pdf"), + It.Is(s => s == "not-really-an-app.pdf"), + It.IsAny(), + It.Is(s => s == "Task_1")), Times.Once); } } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/Action/TestData/UserActionAuthorizerStub.cs b/test/Altinn.App.Core.Tests/Internal/Process/Action/TestData/UserActionAuthorizerStub.cs new file mode 100644 index 000000000..4d3fecb7f --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/Action/TestData/UserActionAuthorizerStub.cs @@ -0,0 +1,13 @@ +using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Action; + +namespace Altinn.App.Core.Tests.Internal.Process.Action.TestData +{ + public class UserActionAuthorizerStub: IUserActionAuthorizer + { + public Task AuthorizeAction(UserActionAuthorizerContext context) + { + return Task.FromResult(true); + } + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtensionTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtensionTests.cs new file mode 100644 index 000000000..222235cdd --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtensionTests.cs @@ -0,0 +1,127 @@ +using Altinn.App.Core.Extensions; +using Altinn.App.Core.Internal.Process.Action; +using Altinn.App.Core.Tests.Internal.Process.Action.TestData; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process.Action; + +public class UserActionAuthorizerServiceCollectionExtensionTests +{ + [Fact] + public void AddTransientUserActionAuthorizerForActionInTask_adds_IUserActinAuthorizerProvider_with_task_and_action_set() + { + // Arrange + string taskId = "Task_1"; + string action = "Action_1"; + IServiceCollection services = new ServiceCollection(); + + // Act + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeFalse(); + services.AddTransientUserActionAuthorizerForActionInTask(taskId, action); + + // Assert + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeTrue(); + services.IsAdded(typeof(UserActionAuthorizerStub)).Should().BeTrue(); + var sp = services.BuildServiceProvider(); + var provider = sp.GetService(); + provider.Should().NotBeNull(); + provider.TaskId.Should().Be(taskId); + provider.Action.Should().Be(action); + provider.Authorizer.Should().BeOfType(); + } + + [Fact] + public void AddTransientUserActionAuthorizerForActionInTask_adds_only_one_UserActionAuthorizerStub_if_used_multiple_times() + { + // Arrange + string taskId = "Task_1"; + string action = "Action_1"; + string taskId2 = "Task_2"; + IServiceCollection services = new ServiceCollection(); + + // Act + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeFalse(); + services.AddTransientUserActionAuthorizerForActionInTask(taskId, action); + services.AddTransientUserActionAuthorizerForActionInTask(taskId2, action); + + // Assert + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeTrue(); + services.IsAdded(typeof(UserActionAuthorizerStub)).Should().BeTrue(); + var sp = services.BuildServiceProvider(); + var authorizer = sp.GetServices(); + authorizer.Should().NotBeNull(); + authorizer.Should().HaveCount(1); + var provider = sp.GetServices(); + provider.Should().NotBeNull(); + provider.Should().HaveCount(2); + provider.Should().ContainEquivalentOf(new UserActionAuthorizerProvider(taskId, action, authorizer.First())); + provider.Should().ContainEquivalentOf(new UserActionAuthorizerProvider(taskId2, action, authorizer.First())); + } + + [Fact] + public void AddTransientUserActionAuthorizerForActionInAllTasks_adds_IUserActinAuthorizerProvider_with_action_set() + { + // Arrange + string action = "Action_1"; + IServiceCollection services = new ServiceCollection(); + + // Act + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeFalse(); + services.AddTransientUserActionAuthorizerForActionInAllTasks(action); + + // Assert + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeTrue(); + services.IsAdded(typeof(UserActionAuthorizerStub)).Should().BeTrue(); + var sp = services.BuildServiceProvider(); + var provider = sp.GetService(); + provider.Should().NotBeNull(); + provider.TaskId.Should().BeNull(); + provider.Action.Should().Be(action); + provider.Authorizer.Should().BeOfType(); + } + + [Fact] + public void AddTransientUserActionAuthorizerForAllActionsInTask_adds_IUserActinAuthorizerProvider_with_task_set() + { + // Arrange + string taskId = "Task_1"; + IServiceCollection services = new ServiceCollection(); + + // Act + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeFalse(); + services.AddTransientUserActionAuthorizerForAllActionsInTask(taskId); + + // Assert + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeTrue(); + services.IsAdded(typeof(UserActionAuthorizerStub)).Should().BeTrue(); + var sp = services.BuildServiceProvider(); + var provider = sp.GetService(); + provider.Should().NotBeNull(); + provider.TaskId.Should().Be(taskId); + provider.Action.Should().BeNull(); + provider.Authorizer.Should().BeOfType(); + } + + [Fact] + public void AddTransientUserActionAuthorizerForAllActionsInAllTasks_adds_IUserActinAuthorizerProvider_without_task_and_action_set() + { + // Arrange + IServiceCollection services = new ServiceCollection(); + + // Act + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeFalse(); + services.AddTransientUserActionAuthorizerForAllActionsInAllTasks(); + + // Assert + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeTrue(); + services.IsAdded(typeof(UserActionAuthorizerStub)).Should().BeTrue(); + var sp = services.BuildServiceProvider(); + var provider = sp.GetService(); + provider.Should().NotBeNull(); + provider.TaskId.Should().BeNull(); + provider.Action.Should().BeNull(); + provider.Authorizer.Should().BeOfType(); + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/Elements/AppProcessStateTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/Elements/AppProcessStateTests.cs new file mode 100644 index 000000000..693d9b09a --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/Elements/AppProcessStateTests.cs @@ -0,0 +1,132 @@ +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process.Elements; + +public class AppProcessStateTests +{ + [Fact] + public void Constructor_with_ProcessState_copies_values() + { + ProcessState input = new ProcessState() + { + Started = DateTime.Now, + StartEvent = "StartEvent", + Ended = DateTime.Now, + EndEvent = "EndEvent", + CurrentTask = new() + { + Started = DateTime.Now, + Ended = DateTime.Now, + Flow = 2, + Name = "Task_1", + Validated = new() + { + Timestamp = DateTime.Now, + CanCompleteTask = false + }, + ElementId = "Task_1", + FlowType = "FlowType", + AltinnTaskType = "data", + } + }; + AppProcessState expected = new AppProcessState() + { + Started = input.Started, + StartEvent = input.StartEvent, + Ended = input.Ended, + EndEvent = input.EndEvent, + CurrentTask = new() + { + Started = input.CurrentTask.Started, + Ended = input.CurrentTask.Ended, + Flow = input.CurrentTask.Flow, + Name = input.CurrentTask.Name, + Validated = new() + { + Timestamp = input.CurrentTask.Validated.Timestamp, + CanCompleteTask = input.CurrentTask.Validated.CanCompleteTask + }, + ElementId = input.CurrentTask.ElementId, + FlowType = input.CurrentTask.FlowType, + AltinnTaskType = input.CurrentTask.AltinnTaskType, + Actions = new Dictionary(), + HasReadAccess = false, + HasWriteAccess = false + } + }; + AppProcessState actual = new(input); + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public void Constructor_with_ProcessState_copies_values_validated_null() + { + ProcessState input = new ProcessState() + { + Started = DateTime.Now, + StartEvent = "StartEvent", + Ended = DateTime.Now, + EndEvent = "EndEvent", + CurrentTask = new() + { + Started = DateTime.Now, + Ended = DateTime.Now, + Flow = 2, + Name = "Task_1", + Validated = null, + ElementId = "Task_1", + FlowType = "FlowType", + AltinnTaskType = "data" + } + }; + AppProcessState expected = new AppProcessState() + { + Started = input.Started, + StartEvent = input.StartEvent, + Ended = input.Ended, + EndEvent = input.EndEvent, + CurrentTask = new() + { + Started = input.CurrentTask.Started, + Ended = input.CurrentTask.Ended, + Flow = input.CurrentTask.Flow, + Name = input.CurrentTask.Name, + Validated = null, + ElementId = input.CurrentTask.ElementId, + FlowType = input.CurrentTask.FlowType, + AltinnTaskType = input.CurrentTask.AltinnTaskType, + Actions = new Dictionary(), + HasReadAccess = false, + HasWriteAccess = false + } + }; + AppProcessState actual = new(input); + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public void Constructor_with_ProcessState_copies_values_currenttask_null() + { + ProcessState input = new ProcessState() + { + Started = DateTime.Now, + StartEvent = "StartEvent", + Ended = DateTime.Now, + EndEvent = "EndEvent", + CurrentTask = null + }; + AppProcessState expected = new AppProcessState() + { + Started = input.Started, + StartEvent = input.StartEvent, + Ended = input.Ended, + EndEvent = input.EndEvent, + CurrentTask = null + }; + AppProcessState actual = new(input); + actual.Should().BeEquivalentTo(expected); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs new file mode 100644 index 000000000..87c4cd8df --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs @@ -0,0 +1,385 @@ +#nullable enable +using System.Text.Json; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Layout; +using Altinn.App.Core.Models.Layout.Components; +using Altinn.App.Core.Models.Process; +using Altinn.App.Core.Tests.Internal.Process.TestData; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process; + +public class ExpressionsExclusiveGatewayTests +{ + [Fact] + public async Task FilterAsync_NoExpressions_ReturnsAllFlows() + { + // Arrange + List dataTypes = new List() + { + new() + { + Id = "test", + AppLogic = new() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.TestData.DummyModel", + } + } + }; + IProcessExclusiveGateway gateway = SetupExpressionsGateway(dataTypes: dataTypes); + var outgoingFlows = new List + { + new SequenceFlow + { + Id = "1", + ConditionExpression = null, + }, + new SequenceFlow + { + Id = "2", + ConditionExpression = null, + }, + }; + var instance = new Instance() + { + Id = "500000/60226acd-b821-4aae-82cd-97a342071bd3", + InstanceOwner = new() + { + PartyId = "500000" + }, + AppId = "ttd/test", + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + }, + Data = new() + { + new() + { + Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", + DataType = "test" + } + } + }; + var processGatewayInformation = new ProcessGatewayInformation + { + Action = "confirm", + }; + + // Act + var result = await gateway.FilterAsync(outgoingFlows, instance, processGatewayInformation); + + // Assert + Assert.Equal(2, result.Count); + Assert.Equal("1", result[0].Id); + Assert.Equal("2", result[1].Id); + } + + [Fact] + public async Task FilterAsync_Expression_filters_based_on_action() + { + // Arrange + List dataTypes = new List() + { + new() + { + Id = "test", + AppLogic = new() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.TestData.DummyModel", + } + } + }; + IProcessExclusiveGateway gateway = SetupExpressionsGateway(dataTypes: dataTypes); + var outgoingFlows = new List + { + new SequenceFlow + { + Id = "1", + ConditionExpression = "[\"equals\", [\"gatewayAction\"], \"confirm\"]", + }, + new SequenceFlow + { + Id = "2", + ConditionExpression = "[\"equals\", [\"gatewayAction\"], \"reject\"]", + }, + }; + var instance = new Instance() + { + Id = "500000/60226acd-b821-4aae-82cd-97a342071bd3", + InstanceOwner = new() + { + PartyId = "500000" + }, + AppId = "ttd/test", + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + }, + Data = new() + { + new() + { + Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", + DataType = "test" + } + } + }; + var processGatewayInformation = new ProcessGatewayInformation + { + Action = "confirm", + }; + + // Act + var result = await gateway.FilterAsync(outgoingFlows, instance, processGatewayInformation); + + // Assert + Assert.Single(result); + Assert.Equal("1", result[0].Id); + } + + [Fact] + public async Task FilterAsync_Expression_filters_based_on_datamodel_set_by_layoutset() + { + // Arrange + List dataTypes = new List() + { + new() + { + Id = "aa", + AppLogic = new() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.TestData.NotFound", + } + }, + new() + { + Id = "test", + AppLogic = new() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.TestData.DummyModel", + } + } + }; + object formData = new DummyModel() + { + Amount = 1000, + Submitter = "test" + }; + LayoutSets layoutSets = new LayoutSets() + { + Sets = new() + { + new() + { + Id = "test", + Tasks = new() { "Task_1" }, + DataType = "test" + } + } + }; + IProcessExclusiveGateway gateway = SetupExpressionsGateway(dataTypes: dataTypes, formData: formData, layoutSets: LayoutSetsToString(layoutSets), dataType: formData.GetType()); + var outgoingFlows = new List + { + new SequenceFlow + { + Id = "1", + ConditionExpression = "[\"notEquals\", [\"dataModel\", \"Amount\"], 1000]", + }, + new SequenceFlow + { + Id = "2", + ConditionExpression = "[\"equals\", [\"dataModel\", \"Amount\"], 1000]", + }, + }; + var instance = new Instance() + { + Id = "500000/60226acd-b821-4aae-82cd-97a342071bd3", + InstanceOwner = new() + { + PartyId = "500000" + }, + AppId = "ttd/test", + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + }, + Data = new() + { + new() + { + Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", + DataType = "test" + } + } + }; + var processGatewayInformation = new ProcessGatewayInformation + { + Action = "confirm", + }; + + // Act + var result = await gateway.FilterAsync(outgoingFlows, instance, processGatewayInformation); + + // Assert + Assert.Single(result); + Assert.Equal("2", result[0].Id); + } + + [Fact] + public async Task FilterAsync_Expression_filters_based_on_datamodel_set_by_gateway() + { + // Arrange + List dataTypes = new List() + { + new() + { + Id = "aa", + AppLogic = new() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.TestData.DummyModel", + } + }, + new() + { + Id = "test", + AppLogic = new() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.TestData.NotFound", + } + } + }; + object formData = new DummyModel() + { + Amount = 1000, + Submitter = "test" + }; + LayoutSets layoutSets = new LayoutSets() + { + Sets = new() + { + new() + { + Id = "test", + Tasks = new() { "Task_1" }, + DataType = "test" + } + } + }; + IProcessExclusiveGateway gateway = SetupExpressionsGateway(dataTypes: dataTypes, formData: formData, layoutSets: LayoutSetsToString(layoutSets), dataType: formData.GetType()); + var outgoingFlows = new List + { + new SequenceFlow + { + Id = "1", + ConditionExpression = "[\"notEquals\", [\"dataModel\", \"Amount\"], 1000]", + }, + new SequenceFlow + { + Id = "2", + ConditionExpression = "[\"equals\", [\"dataModel\", \"Amount\"], 1000]", + }, + }; + var instance = new Instance() + { + Id = "500000/60226acd-b821-4aae-82cd-97a342071bd3", + InstanceOwner = new() + { + PartyId = "500000" + }, + AppId = "ttd/test", + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + }, + Data = new() + { + new() + { + Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", + DataType = "aa" + } + } + }; + var processGatewayInformation = new ProcessGatewayInformation + { + Action = "confirm", + DataTypeId = "aa" + }; + + // Act + var result = await gateway.FilterAsync(outgoingFlows, instance, processGatewayInformation); + + // Assert + Assert.Single(result); + Assert.Equal("2", result[0].Id); + } + + private static ExpressionsExclusiveGateway SetupExpressionsGateway(List dataTypes, string? layoutSets = null, object? formData = null, Type? dataType = null) + { + var resources = new Mock(); + var appModel = new Mock(); + var appMetadata = new Mock(); + var dataClient = new Mock(); + + resources.Setup(r => r.GetLayoutSets()).Returns(layoutSets ?? string.Empty); + appMetadata.Setup(m => m.GetApplicationMetadata()).ReturnsAsync(new ApplicationMetadata("ttd/test-app") + { + DataTypes = dataTypes + }); + resources.Setup(r => r.GetLayoutModel(It.IsAny())).Returns(new LayoutModel() + { + Pages = new Dictionary() + { + { + "Page1", new("Page1", new List(), new Dictionary(), null, null, null, null) + } + } + }); + if (formData != null) + { + dataClient.Setup(d => d.GetFormData(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(formData); + } + + if (dataType != null) + { + appModel.Setup(a => a.GetModelType(dataType.FullName!)).Returns(dataType); + } + + var frontendSettings = Options.Create(new FrontEndSettings()); + var layoutStateInit = new LayoutEvaluatorStateInitializer(resources.Object, frontendSettings); + return new ExpressionsExclusiveGateway(layoutStateInit, resources.Object, appModel.Object, appMetadata.Object, dataClient.Object); + } + + private static string LayoutSetsToString(LayoutSets layoutSets) + { + return JsonSerializer.Serialize(layoutSets, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + }); + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/FlowHydrationTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/FlowHydrationTests.cs deleted file mode 100644 index dbb7709da..000000000 --- a/test/Altinn.App.Core.Tests/Internal/Process/FlowHydrationTests.cs +++ /dev/null @@ -1,265 +0,0 @@ -using System.Collections.Generic; -using Altinn.App.Core.Features; -using Altinn.App.Core.Internal.Process; -using Altinn.App.Core.Internal.Process.Elements; -using Altinn.App.Core.Internal.Process.Elements.Base; -using Altinn.App.PlatformServices.Tests.Internal.Process.StubGatewayFilters; -using Altinn.App.PlatformServices.Tests.Internal.Process.TestUtils; -using Altinn.Platform.Storage.Interface.Models; -using FluentAssertions; -using Xunit; - -namespace Altinn.App.PlatformServices.Tests.Internal.Process; - -public class FlowHydrationTests -{ - [Fact] - public async void NextFollowAndFilterGateways_returns_next_element_if_no_gateway() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-linear.bpmn", new List()); - List nextElements = await flowHydrator.NextFollowAndFilterGateways(new Instance(), "Task1"); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = "Bekreft skjemadata", - TaskType = "confirmation", - Incoming = new List { "Flow2" }, - Outgoing = new List { "Flow3" } - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_returns_empty_list_if_no_outgoing_flows() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-linear.bpmn", new List()); - List nextElements = await flowHydrator.NextFollowAndFilterGateways(new Instance(), "EndEvent"); - nextElements.Should().BeEmpty(); - } - - [Fact] - public async void NextFollowAndFilterGateways_returns_default_if_no_filtering_is_implemented_and_default_set() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway-default.bpmn", new List()); - List nextElements = await flowHydrator.NextFollowAndFilterGateways(new Instance(), "Task1"); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow3" }, - Outgoing = new List { "Flow5" } - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_returns_all_if_no_filtering_is_implemented_and_default_set_but_followDefaults_false() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway-default.bpmn", new List()); - List nextElements = await flowHydrator.NextFollowAndFilterGateways(new Instance(), "Task1", false); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow3" }, - Outgoing = new List { "Flow5" } - }, - new EndEvent() - { - Id = "EndEvent", - Incoming = new List { "Flow5", "Flow4" }, - Name = null!, - Outgoing = new List() - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_returns_all_gateway_target_tasks_if_no_filter_and_default() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway.bpmn", new List()); - List nextElements = await flowHydrator.NextFollowAndFilterGateways(new Instance(), "Task1"); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow3" }, - Outgoing = new List { "Flow5" } - }, - new ProcessTask() - { - Id = "EndEvent", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow4", "Flow5" }, - Outgoing = new List() - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_returns_all_gateway_target_tasks_if_no_filter_and_default_folowDefaults_false() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway.bpmn", new List()); - List nextElements = await flowHydrator.NextFollowAndFilterGateways(new Instance(), "Task1", false); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow3" }, - Outgoing = new List { "Flow5" } - }, - new ProcessTask() - { - Id = "EndEvent", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow4", "Flow5" }, - Outgoing = new List() - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_runs_custom_filter_and_returns_result() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway-with-join-gateway.bpmn", new List() - { - new DataValuesFilter("Gateway1", "choose") - }); - Instance i = new Instance() - { - DataValues = new Dictionary() - { - { "choose", "Flow3" } - } - }; - - List nextElements = await flowHydrator.NextFollowAndFilterGateways(i, "Task1"); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow3" }, - Outgoing = new List { "Flow5" } - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_does_not_run_filter_with_non_matchin_ids() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway-with-join-gateway.bpmn", new List() - { - new DataValuesFilter("Foobar", "choose") - }); - Instance i = new Instance() - { - DataValues = new Dictionary() - { - { "choose", "Flow3" } - } - }; - - List nextElements = await flowHydrator.NextFollowAndFilterGateways(i, "Task1"); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow3" }, - Outgoing = new List { "Flow5" } - }, - new ProcessTask() - { - Id = "EndEvent", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow6" }, - Outgoing = new List() - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_follows_downstream_gateways() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway-with-join-gateway.bpmn", new List()); - List nextElements = await flowHydrator.NextFollowAndFilterGateways(new Instance(), "Task1"); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow3" }, - Outgoing = new List { "Flow5" } - }, - new ProcessTask() - { - Id = "EndEvent", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow6" }, - Outgoing = new List() - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_runs_custom_filter_and_returns_empty_list_if_all_filtered_out() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway-with-join-gateway.bpmn", new List() - { - new DataValuesFilter("Gateway1", "choose1"), - new DataValuesFilter("Gateway2", "choose2") - }); - Instance i = new Instance() - { - DataValues = new Dictionary() - { - { "choose1", "Flow4" }, - { "choose2", "Foobar" } - } - }; - - List nextElements = await flowHydrator.NextFollowAndFilterGateways(i, "Task1"); - nextElements.Should().BeEmpty(); - } - - [Fact] - public async void NextFollowAndFilterGateways_returns_empty_list_if_element_has_no_next() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway-with-join-gateway.bpmn", new List()); - Instance i = new Instance(); - - List nextElements = await flowHydrator.NextFollowAndFilterGateways(i, "EndEvent"); - nextElements.Should().BeEmpty(); - } - - private static IFlowHydration SetupFlowHydration(string bpmnfile, IEnumerable gatewayFilters) - { - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - return new FlowHydration(pr, new ExclusiveGatewayFactory(gatewayFilters)); - } -} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineMetricsDecoratorTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineMetricsDecoratorTests.cs new file mode 100644 index 000000000..7ba00c9df --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineMetricsDecoratorTests.cs @@ -0,0 +1,304 @@ +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Models.Process; +using Altinn.App.Core.Tests.TestHelpers; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Moq; +using Prometheus; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process; + +public class ProcessEngineMetricsDecoratorTests +{ + public ProcessEngineMetricsDecoratorTests() + { + Metrics.SuppressDefaultMetrics(); + } + + [Fact] + public async Task StartProcess_calls_decorated_service_and_increments_success_counter_when_successful() + { + // Arrange + var processEngine = new Mock(); + processEngine.Setup(p => p.StartProcess(It.IsAny())).ReturnsAsync(new ProcessChangeResult { Success = true }); + var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); + (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_start_count{result=\"success\"}"); + + var result = decorator.StartProcess(new ProcessStartRequest()); + + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_start_count{result=\"success\"} 1"); + result.Result.Success.Should().BeTrue(); + result = decorator.StartProcess(new ProcessStartRequest()); + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_start_count{result=\"success\"} 2"); + result.Result.Success.Should().BeTrue(); + processEngine.Verify(p => p.StartProcess(It.IsAny()), Times.Exactly(2)); + processEngine.VerifyNoOtherCalls(); + } + + [Fact] + public async Task StartProcess_calls_decorated_service_and_increments_failure_counter_when_unsuccessful() + { + // Arrange + var processEngine = new Mock(); + processEngine.Setup(p => p.StartProcess(It.IsAny())).ReturnsAsync(new ProcessChangeResult { Success = false }); + var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); + (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_start_count{result=\"failure\"}"); + + var result = decorator.StartProcess(new ProcessStartRequest()); + + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_start_count{result=\"failure\"} 1"); + result.Result.Success.Should().BeFalse(); + result = decorator.StartProcess(new ProcessStartRequest()); + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_start_count{result=\"failure\"} 2"); + result.Result.Success.Should().BeFalse(); + processEngine.Verify(p => p.StartProcess(It.IsAny()), Times.Exactly(2)); + processEngine.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Next_calls_decorated_service_and_increments_success_counter_when_successful() + { + // Arrange + var processEngine = new Mock(); + processEngine.Setup(p => p.Next(It.IsAny())).ReturnsAsync(new ProcessChangeResult { Success = true }); + var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); + (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_task_next_count{result=\"success\",action=\"write\",task=\"Task_1\"}"); + + var result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }, + Action = "write" + }); + + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_task_next_count{result=\"success\",action=\"write\",task=\"Task_1\"} 1"); + result.Result.Success.Should().BeTrue(); + result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }, + Action = "write" + }); + var prometheusMetricsToString = await ReadPrometheusMetricsToString(); + prometheusMetricsToString.Should().Contain("altinn_app_process_task_next_count{result=\"success\",action=\"write\",task=\"Task_1\"} 2"); + result.Result.Success.Should().BeTrue(); + processEngine.Verify(p => p.Next(It.IsAny()), Times.Exactly(2)); + processEngine.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Next_calls_decorated_service_and_increments_failure_counter_when_unsuccessful() + { + // Arrange + var processEngine = new Mock(); + processEngine.Setup(p => p.Next(It.IsAny())).ReturnsAsync(new ProcessChangeResult { Success = false }); + var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); + (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_task_next_count{result=\"failure\",action=\"write\",task=\"Task_1\"}"); + + var result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }, + Action = "write" + }); + + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_task_next_count{result=\"failure\",action=\"write\",task=\"Task_1\"} 1"); + result.Result.Success.Should().BeFalse(); + result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }, + Action = "write" + }); + var prometheusMetricsToString = await ReadPrometheusMetricsToString(); + prometheusMetricsToString.Should().Contain("altinn_app_process_task_next_count{result=\"failure\",action=\"write\",task=\"Task_1\"} 2"); + result.Result.Success.Should().BeFalse(); + processEngine.Verify(p => p.Next(It.IsAny()), Times.Exactly(2)); + processEngine.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Next_calls_decorated_service_and_increments_success_and_end_counters_when_successful_and_process_ended() + { + // Arrange + var processEngine = new Mock(); + var ended = DateTime.Now; + var started = ended.AddSeconds(-20); + processEngine.Setup(p => p.Next(It.IsAny())).ReturnsAsync(new ProcessChangeResult + { + Success = true, + ProcessStateChange = new() + { + NewProcessState = new() + { + Ended = ended, + Started = started + } + } + }); + var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); + (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_task_next_count{result=\"success\",action=\"confirm\",task=\"Task_2\"}"); + (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_end_count{result=\"success\"}"); + (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_end_time_total{result=\"success\"}"); + + var result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_2" + } + } + }, + Action = "confirm" + }); + + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_task_next_count{result=\"success\",action=\"confirm\",task=\"Task_2\"} 1"); + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_end_count{result=\"success\"} 1"); + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_end_time_total{result=\"success\"} 20"); + result.Result.Success.Should().BeTrue(); + result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_2" + } + } + }, + Action = "confirm" + }); + var prometheusMetricsToString = await ReadPrometheusMetricsToString(); + prometheusMetricsToString.Should().Contain("altinn_app_process_task_next_count{result=\"success\",action=\"confirm\",task=\"Task_2\"} 2"); + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_end_count{result=\"success\"} 2"); + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_end_time_total{result=\"success\"} 40"); + result.Result.Success.Should().BeTrue(); + processEngine.Verify(p => p.Next(It.IsAny()), Times.Exactly(2)); + processEngine.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Next_calls_decorated_service_and_increments_failure_and_end_counters_when_unsuccessful_and_process_ended_no_time_added_if_started_null() + { + // Arrange + var processEngine = new Mock(); + var ended = DateTime.Now; + processEngine.Setup(p => p.Next(It.IsAny())).ReturnsAsync(new ProcessChangeResult + { + Success = false, + ProcessStateChange = new() + { + NewProcessState = new() + { + Ended = ended + } + } + }); + var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); + var prometheusMetricsToString = await ReadPrometheusMetricsToString(); + prometheusMetricsToString.Should().NotContain("altinn_app_process_task_next_count{result=\"failure\",action=\"confirm\",task=\"Task_3\"}"); + prometheusMetricsToString.Should().NotContain("altinn_app_process_end_count{result=\"failure\"}"); + prometheusMetricsToString.Should().NotContain("altinn_app_process_end_time_total{result=\"failure\"}"); + + var result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_3" + } + } + }, + Action = "confirm" + }); + + prometheusMetricsToString = await ReadPrometheusMetricsToString(); + prometheusMetricsToString.Should().Contain("altinn_app_process_task_next_count{result=\"failure\",action=\"confirm\",task=\"Task_3\"} 1"); + prometheusMetricsToString.Should().Contain("altinn_app_process_end_count{result=\"failure\"} 1"); + prometheusMetricsToString.Should().NotContain("altinn_app_process_end_time_total{result=\"failure\"}"); + result.Result.Success.Should().BeFalse(); + result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_3" + } + } + }, + Action = "confirm" + }); + prometheusMetricsToString = await ReadPrometheusMetricsToString(); + prometheusMetricsToString.Should().Contain("altinn_app_process_task_next_count{result=\"failure\",action=\"confirm\",task=\"Task_3\"} 2"); + prometheusMetricsToString.Should().Contain("altinn_app_process_end_count{result=\"failure\"} 2"); + prometheusMetricsToString.Should().NotContain("altinn_app_process_end_time_total{result=\"failure\"}"); + result.Result.Success.Should().BeFalse(); + processEngine.Verify(p => p.Next(It.IsAny()), Times.Exactly(2)); + processEngine.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateInstanceAndRerunEvents_calls_decorated_service() + { + // Arrange + var processEngine = new Mock(); + processEngine.Setup(p => p.UpdateInstanceAndRerunEvents(It.IsAny(), It.IsAny>())).ReturnsAsync(new Instance { }); + var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); + (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_start_count{result=\"success\"}"); + + await decorator.UpdateInstanceAndRerunEvents(new ProcessStartRequest(), new List()); + + processEngine.Verify(p => p.UpdateInstanceAndRerunEvents(It.IsAny(), It.IsAny>()), Times.Once); + processEngine.VerifyNoOtherCalls(); + } + + private static async Task ReadPrometheusMetricsToString() + { + return await PrometheusTestHelper.ReadPrometheusMetricsToString(); + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index 62d4a6f56..8b50955c0 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -1,161 +1,940 @@ -using System.Collections.Generic; -using System.IO; -using Altinn.App.Core.Configuration; +#nullable enable +using System.Security.Claims; +using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; -using Altinn.App.Core.Infrastructure.Clients.Storage; +using Altinn.App.Core.Features.Action; using Altinn.App.Core.Internal.Process; -using Altinn.App.Core.Models; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Models.Process; +using Altinn.Platform.Profile.Models; +using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; +using AltinnCore.Authentication.Constants; +using FluentAssertions; +using Moq; +using Newtonsoft.Json; using Xunit; -namespace Altinn.App.PlatformServices.Tests.Internal.Process -{ - /// - /// Test clas for SimpleInstanceMapper - /// - public class ProcessEngineTest - { - [Fact] - public async void MissingCurrentTask() - { - IProcessReader processReader = GetProcessReader(); +namespace Altinn.App.Core.Tests.Internal.Process; - ProcessEngine processEngine = new ProcessEngine(null!, processReader, GetFlowHydration(processReader)); +public class ProcessEngineTest : IDisposable +{ + private Mock _processReaderMock; + private readonly Mock _profileMock; + private readonly Mock _processNavigatorMock; + private readonly Mock _processEventDispatcherMock; - Instance instance = new Instance - { - Process = new ProcessState() - }; + public ProcessEngineTest() + { + _processReaderMock = new(); + _profileMock = new(); + _processNavigatorMock = new(); + _processEventDispatcherMock = new(); + } - ProcessChangeContext processChangeContext = new ProcessChangeContext(instance, null!); + [Fact] + public async Task StartProcess_returns_unsuccessful_when_process_already_started() + { + IProcessEngine processEngine = GetProcessEngine(); + Instance instance = new Instance() { Process = new ProcessState() { CurrentTask = new ProcessElementInfo() { ElementId = "Task_1" } } }; + ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance }; + ProcessChangeResult result = await processEngine.StartProcess(processStartRequest); + result.Success.Should().BeFalse(); + result.ErrorMessage.Should().Be("Process is already started. Use next."); + result.ErrorType.Should().Be(ProcessErrorType.Conflict); + } - processChangeContext = await processEngine.Next(processChangeContext); + [Fact] + public async Task StartProcess_returns_unsuccessful_when_no_matching_startevent_found() + { + Mock processReaderMock = new(); + processReaderMock.Setup(r => r.GetStartEventIds()).Returns(new List() { "StartEvent_1" }); + IProcessEngine processEngine = GetProcessEngine(processReaderMock); + Instance instance = new Instance(); + ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance, StartEventId = "NotTheStartEventYouAreLookingFor" }; + ProcessChangeResult result = await processEngine.StartProcess(processStartRequest); + _processReaderMock.Verify(r => r.GetStartEventIds(), Times.Once); + result.Success.Should().BeFalse(); + result.ErrorMessage.Should().Be("No matching startevent"); + result.ErrorType.Should().Be(ProcessErrorType.Conflict); + } - Assert.True(processChangeContext.FailedProcessChange); - Assert.Equal("Instance does not have current task information!", processChangeContext.ProcessMessages[0].Message); - } + [Fact] + public async Task StartProcess_starts_process_and_moves_to_first_task_without_event_dispatch_when_dryrun() + { + IProcessEngine processEngine = GetProcessEngine(); + Instance instance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + } + }; + ClaimsPrincipal user = new(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd"), + })); + ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance, User = user, Dryrun = true }; + ProcessChangeResult result = await processEngine.StartProcess(processStartRequest); + _processReaderMock.Verify(r => r.GetStartEventIds(), Times.Once); + _processReaderMock.Verify(r => r.IsProcessTask("StartEvent_1"), Times.Once); + _processReaderMock.Verify(r => r.IsEndEvent("Task_1"), Times.Once); + _processReaderMock.Verify(r => r.IsProcessTask("Task_1"), Times.Once); + _processNavigatorMock.Verify(n => n.GetNextTask(It.IsAny(), "StartEvent_1", null), Times.Once); + result.Success.Should().BeTrue(); + } - [Fact] - public async void RequestingCurrentTask() + [Fact] + public async Task StartProcess_starts_process_and_moves_to_first_task() + { + IProcessEngine processEngine = GetProcessEngine(); + Instance instance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + } + }; + ClaimsPrincipal user = new(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd"), + })); + ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance, User = user }; + ProcessChangeResult result = await processEngine.StartProcess(processStartRequest); + _processReaderMock.Verify(r => r.GetStartEventIds(), Times.Once); + _processReaderMock.Verify(r => r.IsProcessTask("StartEvent_1"), Times.Once); + _processReaderMock.Verify(r => r.IsEndEvent("Task_1"), Times.Once); + _processReaderMock.Verify(r => r.IsProcessTask("Task_1"), Times.Once); + _processNavigatorMock.Verify(n => n.GetNextTask(It.IsAny(), "StartEvent_1", null), Times.Once); + var expectedInstance = new Instance() { - IProcessReader processReader = GetProcessReader(); + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + ElementId = "Task_1", + Flow = 2, + AltinnTaskType = "data", + FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), + Name = "Utfylling" + }, + StartEvent = "StartEvent_1" + } + }; + var expectedInstanceEvents = new List() + { + new() + { + EventType = InstanceEventType.process_StartEvent.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2 + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "StartEvent_1", + Flow = 1, + Validated = new() + { + CanCompleteTask = false + } + } + } + }, - ProcessEngine processEngine = new ProcessEngine(null!, processReader, GetFlowHydration(processReader)); + new() + { + EventType = InstanceEventType.process_StartTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2, + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_1", + Name = "Utfylling", + AltinnTaskType = "data", + Flow = 2, + Validated = new() + { + CanCompleteTask = false + } + } + } + } + }; + _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + It.Is(i => CompareInstance(expectedInstance, i)), + null, + It.Is>(l => CompareInstanceEvents(expectedInstanceEvents, l)))); + result.Success.Should().BeTrue(); + } - Instance instance = new Instance + [Fact] + public async Task StartProcess_starts_process_and_moves_to_first_task_with_prefill() + { + IProcessEngine processEngine = GetProcessEngine(); + Instance instance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + } + }; + ClaimsPrincipal user = new(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd"), + })); + var prefill = new Dictionary() { { "test", "test" } }; + ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance, User = user, Prefill = prefill }; + ProcessChangeResult result = await processEngine.StartProcess(processStartRequest); + _processReaderMock.Verify(r => r.GetStartEventIds(), Times.Once); + _processReaderMock.Verify(r => r.IsProcessTask("StartEvent_1"), Times.Once); + _processReaderMock.Verify(r => r.IsEndEvent("Task_1"), Times.Once); + _processReaderMock.Verify(r => r.IsProcessTask("Task_1"), Times.Once); + _processNavigatorMock.Verify(n => n.GetNextTask(It.IsAny(), "StartEvent_1", null), Times.Once); + var expectedInstance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + ElementId = "Task_1", + Flow = 2, + AltinnTaskType = "data", + FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), + Name = "Utfylling" + }, + StartEvent = "StartEvent_1" + } + }; + var expectedInstanceEvents = new List() + { + new() { - Process = new ProcessState + EventType = InstanceEventType.process_StartEvent.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2 + }, + ProcessInfo = new() { - CurrentTask = new ProcessElementInfo() { ElementId = "Task_1" } + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "StartEvent_1", + Flow = 1, + Validated = new() + { + CanCompleteTask = false + } + } } - }; + }, - ProcessChangeContext processChangeContext = new ProcessChangeContext(instance, null!) + new() { - RequestedProcessElementId = "Task_1" - }; + EventType = InstanceEventType.process_StartTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2, + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_1", + Name = "Utfylling", + AltinnTaskType = "data", + Flow = 2, + Validated = new() + { + CanCompleteTask = false + } + } + } + } + }; + _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + It.Is(i => CompareInstance(expectedInstance, i)), + prefill, + It.Is>(l => CompareInstanceEvents(l, expectedInstanceEvents)))); + result.Success.Should().BeTrue(); + } - processChangeContext = await processEngine.Next(processChangeContext); + [Fact] + public async Task Next_returns_unsuccessful_when_process_null() + { + IProcessEngine processEngine = GetProcessEngine(); + Instance instance = new Instance() { Process = null }; + ProcessNextRequest processNextRequest = new ProcessNextRequest() { Instance = instance }; + ProcessChangeResult result = await processEngine.Next(processNextRequest); + result.Success.Should().BeFalse(); + result.ErrorMessage.Should().Be("Instance does not have current task information!"); + result.ErrorType.Should().Be(ProcessErrorType.Conflict); + } - Assert.True(processChangeContext.FailedProcessChange); - Assert.Equal("Requested process element Task_1 is same as instance's current task. Cannot change process.", processChangeContext.ProcessMessages[0].Message); - } + [Fact] + public async Task Next_returns_unsuccessful_when_process_currenttask_null() + { + IProcessEngine processEngine = GetProcessEngine(); + Instance instance = new Instance() { Process = new ProcessState() { CurrentTask = null } }; + ProcessNextRequest processNextRequest = new ProcessNextRequest() { Instance = instance }; + ProcessChangeResult result = await processEngine.Next(processNextRequest); + result.Success.Should().BeFalse(); + result.ErrorMessage.Should().Be("Instance does not have current task information!"); + result.ErrorType.Should().Be(ProcessErrorType.Conflict); + } - [Fact] - public async void RequestInvalidTask() + [Fact] + public async Task Next_moves_instance_to_next_task_and_produces_instanceevents() + { + var expectedInstance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + ElementId = "Task_2", + Flow = 3, + AltinnTaskType = "confirmation", + FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), + Name = "Bekreft" + }, + StartEvent = "StartEvent_1" + } + }; + IProcessEngine processEngine = GetProcessEngine(null, expectedInstance); + Instance instance = new Instance() + { + InstanceOwner = new() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_1", + AltinnTaskType = "data", + Flow = 2, + Validated = new() + { + CanCompleteTask = true + } + } + } + }; + ProcessState originalProcessState = instance.Process.Copy(); + ClaimsPrincipal user = new(new ClaimsIdentity(new List() { - IProcessReader processReader = GetProcessReader(); + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd"), + })); + ProcessNextRequest processNextRequest = new ProcessNextRequest() { Instance = instance, User = user }; + ProcessChangeResult result = await processEngine.Next(processNextRequest); + _processReaderMock.Verify(r => r.IsProcessTask("Task_1"), Times.Once); + _processReaderMock.Verify(r => r.IsEndEvent("Task_2"), Times.Once); + _processReaderMock.Verify(r => r.IsProcessTask("Task_2"), Times.Once); + _processNavigatorMock.Verify(n => n.GetNextTask(It.IsAny(), "Task_1", null), Times.Once); + + var expectedInstanceEvents = new List() + { + new() + { + EventType = InstanceEventType.process_EndTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2 + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_1", + Flow = 2, + AltinnTaskType = "data", + Validated = new() + { + CanCompleteTask = true + } + } + } + }, - ProcessEngine processEngine = new ProcessEngine(null!, processReader, GetFlowHydration(processReader)); + new() + { + EventType = InstanceEventType.process_StartTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2, + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_2", + Name = "Bekreft", + AltinnTaskType = "confirmation", + FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), + Flow = 3 + } + } + } + }; + _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + It.Is(i => CompareInstance(expectedInstance, i)), + It.IsAny?>(), + It.Is>(l => CompareInstanceEvents(expectedInstanceEvents, l)))); + _processEventDispatcherMock.Verify(d => d.RegisterEventWithEventsComponent(It.Is(i => CompareInstance(expectedInstance, i)))); + result.Success.Should().BeTrue(); + result.ProcessStateChange.Should().BeEquivalentTo( + new ProcessStateChange() + { + Events = expectedInstanceEvents, + NewProcessState = expectedInstance.Process, + OldProcessState = originalProcessState + }); + } - Instance instance = new Instance + [Fact] + public async Task Next_moves_instance_to_next_task_and_produces_abandon_instanceevent_when_action_reject() + { + var expectedInstance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + ElementId = "Task_2", + Flow = 3, + AltinnTaskType = "confirmation", + FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), + Name = "Bekreft" + }, + StartEvent = "StartEvent_1" + } + }; + IProcessEngine processEngine = GetProcessEngine(null, expectedInstance); + Instance instance = new Instance() + { + InstanceOwner = new() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_1", + AltinnTaskType = "data", + Flow = 2, + Validated = new() + { + CanCompleteTask = true + } + } + } + }; + ProcessState originalProcessState = instance.Process.Copy(); + ClaimsPrincipal user = new(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd"), + })); + ProcessNextRequest processNextRequest = new ProcessNextRequest() { Instance = instance, User = user, Action = "reject" }; + ProcessChangeResult result = await processEngine.Next(processNextRequest); + _processReaderMock.Verify(r => r.IsProcessTask("Task_1"), Times.Once); + _processReaderMock.Verify(r => r.IsEndEvent("Task_2"), Times.Once); + _processReaderMock.Verify(r => r.IsProcessTask("Task_2"), Times.Once); + _processNavigatorMock.Verify(n => n.GetNextTask(It.IsAny(), "Task_1", "reject"), Times.Once); + + var expectedInstanceEvents = new List() + { + new() { - Process = new ProcessState + EventType = InstanceEventType.process_AbandonTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2 + }, + ProcessInfo = new() { - CurrentTask = new ProcessElementInfo() { ElementId = "Task_1" } + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_1", + Flow = 2, + AltinnTaskType = "data", + Validated = new() + { + CanCompleteTask = true + } + } } - }; + }, - ProcessChangeContext processChangeContext = new ProcessChangeContext(instance, null!) + new() { - RequestedProcessElementId = "Task_10" - }; + EventType = InstanceEventType.process_StartTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2, + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_2", + Name = "Bekreft", + AltinnTaskType = "confirmation", + FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), + Flow = 3 + } + } + } + }; + _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + It.Is(i => CompareInstance(expectedInstance, i)), + It.IsAny?>(), + It.Is>(l => CompareInstanceEvents(expectedInstanceEvents, l)))); + _processEventDispatcherMock.Verify(d => d.RegisterEventWithEventsComponent(It.Is(i => CompareInstance(expectedInstance, i)))); + result.Success.Should().BeTrue(); + result.ProcessStateChange.Should().BeEquivalentTo( + new ProcessStateChange() + { + Events = expectedInstanceEvents, + NewProcessState = expectedInstance.Process, + OldProcessState = originalProcessState + }); + } - processChangeContext = await processEngine.Next(processChangeContext); + [Fact] + public async Task Next_moves_instance_to_end_event_and_ends_proces() + { + var expectedInstance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + CurrentTask = null, + StartEvent = "StartEvent_1", + EndEvent = "EndEvent_1" + } + }; + IProcessEngine processEngine = GetProcessEngine(null, expectedInstance); + Instance instance = new Instance() + { + InstanceOwner = new() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_2", + AltinnTaskType = "confirmation", + Flow = 3, + Validated = new() + { + CanCompleteTask = true + } + } + } + }; + ProcessState originalProcessState = instance.Process.Copy(); + ClaimsPrincipal user = new(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + })); + ProcessNextRequest processNextRequest = new ProcessNextRequest() { Instance = instance, User = user }; + ProcessChangeResult result = await processEngine.Next(processNextRequest); + _processReaderMock.Verify(r => r.IsProcessTask("Task_2"), Times.Once); + _processReaderMock.Verify(r => r.IsEndEvent("EndEvent_1"), Times.Once); + _profileMock.Verify(p => p.GetUserProfile(1337), Times.Exactly(3)); + _processNavigatorMock.Verify(n => n.GetNextTask(It.IsAny(), "Task_2", null), Times.Once); + + var expectedInstanceEvents = new List() + { + new() + { + EventType = InstanceEventType.process_EndTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + AuthenticationLevel = 2, + NationalIdentityNumber = "22927774937" + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_2", + Flow = 3, + AltinnTaskType = "confirmation", + Validated = new() + { + CanCompleteTask = true + } + } + } + }, - Assert.True(processChangeContext.FailedProcessChange); - Assert.Contains("The proposed next element id 'Task_10' is", processChangeContext.ProcessMessages[0].Message); - } + new() + { + EventType = InstanceEventType.process_EndEvent.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + NationalIdentityNumber = "22927774937", + AuthenticationLevel = 2, + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = null, + EndEvent = "EndEvent_1" + } + }, + new() + { + EventType = InstanceEventType.Submited.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + NationalIdentityNumber = "22927774937", + AuthenticationLevel = 2, + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = null, + EndEvent = "EndEvent_1" + } + } + }; + _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + It.Is(i => CompareInstance(expectedInstance, i)), + It.IsAny?>(), + It.Is>(l => CompareInstanceEvents(expectedInstanceEvents, l)))); + _processEventDispatcherMock.Verify(d => d.RegisterEventWithEventsComponent(It.Is(i => CompareInstance(expectedInstance, i)))); + result.Success.Should().BeTrue(); + result.ProcessStateChange.Should().BeEquivalentTo( + new ProcessStateChange() + { + Events = expectedInstanceEvents, + NewProcessState = expectedInstance.Process, + OldProcessState = originalProcessState + }); + } - [Fact] - public async void StartStartedTask() + [Fact] + public async Task UpdateInstanceAndRerunEvents_sends_instance_and_events_to_eventdispatcher() + { + Instance instance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + StartEvent = "StartEvent_1", + CurrentTask = new ProcessElementInfo() + { + ElementId = "Task_1", + Flow = 3, + AltinnTaskType = "confirmation", + Validated = new() + { + CanCompleteTask = true + } + } + } + }; + Instance updatedInstance = new Instance() + { + Org = "ttd", + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + StartEvent = "StartEvent_1", + CurrentTask = new ProcessElementInfo() + { + ElementId = "Task_1", + Flow = 3, + AltinnTaskType = "confirmation", + Validated = new() + { + CanCompleteTask = true + } + } + } + }; + Dictionary prefill = new Dictionary() { - IProcessReader processReader = GetProcessReader(); + { "test", "test" } + }; + List events = new List() + { + new() + { + EventType = InstanceEventType.process_AbandonTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2 + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_1", + Flow = 2, + AltinnTaskType = "data", + Validated = new() + { + CanCompleteTask = true + } + } + } + } + }; + IProcessEngine processEngine = GetProcessEngine(null, updatedInstance); + ProcessStartRequest processStartRequest = new ProcessStartRequest() + { + Instance = instance, + Prefill = prefill, + }; + Instance result = await processEngine.UpdateInstanceAndRerunEvents(processStartRequest, events); + _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + It.Is(i => CompareInstance(instance, i)), + prefill, + It.Is>(l => CompareInstanceEvents(events, l)))); + result.Should().Be(updatedInstance); + } - ProcessEngine processEngine = new ProcessEngine(null!, processReader, GetFlowHydration(processReader)); + private IProcessEngine GetProcessEngine(Mock? processReaderMock = null, Instance? updatedInstance = null) + { + if (processReaderMock == null) + { + _processReaderMock = new(); + _processReaderMock.Setup(r => r.GetStartEventIds()).Returns(new List() { "StartEvent_1" }); + _processReaderMock.Setup(r => r.IsProcessTask("StartEvent_1")).Returns(false); + _processReaderMock.Setup(r => r.IsEndEvent("Task_1")).Returns(false); + _processReaderMock.Setup(r => r.IsProcessTask("Task_1")).Returns(true); + _processReaderMock.Setup(r => r.IsProcessTask("Task_2")).Returns(true); + _processReaderMock.Setup(r => r.IsProcessTask("EndEvent_1")).Returns(false); + _processReaderMock.Setup(r => r.IsEndEvent("EndEvent_1")).Returns(true); + _processReaderMock.Setup(r => r.IsProcessTask("EndEvent_1")).Returns(false); + } + else + { + _processReaderMock = processReaderMock; + } - Instance instance = new Instance + _profileMock.Setup(p => p.GetUserProfile(1337)).ReturnsAsync(() => new UserProfile() + { + UserId = 1337, + Email = "test@example.com", + Party = new Party() { - Process = new ProcessState + SSN = "22927774937" + } + }); + _processNavigatorMock.Setup( + pn => pn.GetNextTask(It.IsAny(), "StartEvent_1", It.IsAny())) + .ReturnsAsync(() => new ProcessTask() + { + Id = "Task_1", + Incoming = new List { "Flow_1" }, + Outgoing = new List { "Flow_2" }, + Name = "Utfylling", + ExtensionElements = new() { - CurrentTask = new ProcessElementInfo() { ElementId = "Task_1" } + TaskExtension = new() + { + TaskType = "data" + } } - }; - - ProcessChangeContext processChangeContext = new ProcessChangeContext(instance, null!) + }); + _processNavigatorMock.Setup( + pn => pn.GetNextTask(It.IsAny(), "Task_1", It.IsAny())) + .ReturnsAsync(() => new ProcessTask() + { + Id = "Task_2", + Incoming = new List { "Flow_2" }, + Outgoing = new List { "Flow_3" }, + Name = "Bekreft", + ExtensionElements = new() + { + TaskExtension = new() + { + TaskType = "confirmation" + } + } + }); + _processNavigatorMock.Setup( + pn => pn.GetNextTask(It.IsAny(), "Task_2", It.IsAny())) + .ReturnsAsync(() => new EndEvent() { - RequestedProcessElementId = "Task_10" - }; + Id = "EndEvent_1", + Incoming = new List { "Flow_3" } + }); + if (updatedInstance is not null) + { + _processEventDispatcherMock.Setup(d => d.UpdateProcessAndDispatchEvents(It.IsAny(), It.IsAny?>(), It.IsAny>())) + .ReturnsAsync(() => updatedInstance); + } - processChangeContext = await processEngine.StartProcess(processChangeContext); + return new ProcessEngine( + _processReaderMock.Object, + _profileMock.Object, + _processNavigatorMock.Object, + _processEventDispatcherMock.Object, + new UserActionService(new List())); + } - Assert.True(processChangeContext.FailedProcessChange); - Assert.Contains("Process is already started. Use next.", processChangeContext.ProcessMessages[0].Message); - } + public void Dispose() + { + _processReaderMock.VerifyNoOtherCalls(); + _profileMock.VerifyNoOtherCalls(); + _processNavigatorMock.VerifyNoOtherCalls(); + _processEventDispatcherMock.VerifyNoOtherCalls(); + } - [Fact] - public async void InvalidStartEvent() + private static bool CompareInstance(Instance expected, Instance actual) + { + expected.Process.Started = actual.Process.Started; + expected.Process.Ended = actual.Process.Ended; + if (actual.Process.CurrentTask != null) { - IProcessReader processReader = GetProcessReader(); + expected.Process.CurrentTask.Started = actual.Process.CurrentTask.Started; + } - ProcessEngine processEngine = new ProcessEngine(null!, processReader, GetFlowHydration(processReader)); + return JsonCompare(expected, actual); + } - Instance instance = new Instance(); - - ProcessChangeContext processChangeContext = new ProcessChangeContext(instance, null!) + private static bool CompareInstanceEvents(List expected, List actual) + { + for (int i = 0; i < expected.Count; i++) + { + expected[i].Created = actual[i].Created; + expected[i].ProcessInfo.Started = actual[i].ProcessInfo.Started; + expected[i].ProcessInfo.Ended = actual[i].ProcessInfo.Ended; + if (actual[i].ProcessInfo.CurrentTask != null) { - RequestedProcessElementId = "Task_10" - }; + expected[i].ProcessInfo.CurrentTask.Started = actual[i].ProcessInfo.CurrentTask.Started; + } + } - processChangeContext = await processEngine.StartProcess(processChangeContext); + return JsonCompare(expected, actual); + } - Assert.True(processChangeContext.FailedProcessChange); - Assert.Contains("No matching startevent", processChangeContext.ProcessMessages[0].Message); + public static bool JsonCompare(object expected, object actual) + { + if (ReferenceEquals(expected, actual)) + { + return true; } - private static IProcessReader GetProcessReader() + if ((expected == null) || (actual == null)) { - AppSettings appSettings = new AppSettings - { - AppBasePath = Path.Join("Internal", "Process", "TestData", "ProcessEngineTest") + Path.DirectorySeparatorChar - }; - IOptions appSettingsO = Microsoft.Extensions.Options.Options.Create(appSettings); - - PlatformSettings platformSettings = new PlatformSettings - { - ApiStorageEndpoint = "http://localhost/" - }; - IOptions platformSettings0 = Microsoft.Extensions.Options.Options.Create(platformSettings); + return false; + } - ProcessClient processClient = new ProcessClient(platformSettings0, appSettingsO, null!, new NullLogger(), null!, new System.Net.Http.HttpClient()); - return new ProcessReader(processClient); + if (expected.GetType() != actual.GetType()) + { + return false; } - private static IFlowHydration GetFlowHydration(IProcessReader processReader) + var expectedJson = JsonConvert.SerializeObject(expected); + var actualJson = JsonConvert.SerializeObject(actual); + + var jsonCompare = expectedJson == actualJson; + if (jsonCompare == false) { - return new FlowHydration(processReader, new ExclusiveGatewayFactory(new List())); + Console.WriteLine("Not equal"); } + + return jsonCompare; } } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs new file mode 100644 index 000000000..342e97f17 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs @@ -0,0 +1,825 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Events; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Process; +using Altinn.Platform.Storage.Interface.Enums; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process; + +public class ProcessEventDispatcherTests +{ + [Fact] + public async Task UpdateProcessAndDispatchEvents_StartEvent_instance_updated_and_events_sent_to_storage_nothing_sent_to_ITask() + { + // Arrange + var instanceService = new Mock(); + var instanceEvent = new Mock(); + var taskEvents = new Mock(); + var appEvents = new Mock(); + var eventsService = new Mock(); + var appSettings = Options.Create(new AppSettings()); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Org = "ttd", + AppId = "ttd/test-app", + }; + Instance updateInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }; + Instance getInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_1", + Flow = 2 + } + } + }; + List events = new List() + { + new InstanceEvent() + { + EventType = InstanceEventType.process_StartEvent.ToString(), + ProcessInfo = new() + { + CurrentTask = new() + { + ElementId = "StartEvent", + AltinnTaskType = "start", + Name = "Start" + } + } + } + }; + instanceService.Setup(i => i.UpdateProcess(instance)).ReturnsAsync(updateInstanceResponse); + instanceService.Setup(i => i.GetInstance(updateInstanceResponse)).ReturnsAsync(getInstanceResponse); + Dictionary prefill = new Dictionary(); + + // Act + var result = await dispatcher.UpdateProcessAndDispatchEvents(instance, prefill, events); + + // Assert + result.Should().Be(getInstanceResponse); + instanceService.Verify(i => i.UpdateProcess(instance), Times.Once); + instanceService.Verify(i => i.GetInstance(updateInstanceResponse), Times.Once); + instanceEvent.Verify(p => p.SaveInstanceEvent(events[0], instance.Org, "test-app"), Times.Once); + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateProcessAndDispatchEvents_StartTask_instance_updated_and_events_sent_to_storage_nothing_sent_to_ITask_when_tasktype_missing() + { + // Arrange + var instanceService = new Mock(); + var instanceEvent = new Mock(); + var taskEvents = new Mock(); + var appEvents = new Mock(); + var eventsService = new Mock(); + var appSettings = Options.Create(new AppSettings()); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Org = "ttd", + AppId = "ttd/test-app", + }; + Instance updateInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }; + Instance getInstanceResponse = new Instance() + { + Id = instance.Id, + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_1", + Flow = 2 + } + } + }; + List events = new List() + { + new InstanceEvent() + { + EventType = InstanceEventType.process_StartTask.ToString(), + ProcessInfo = new() + { + CurrentTask = new() + { + ElementId = "StartEvent", + Name = "Start" + } + } + } + }; + instanceService.Setup(i => i.UpdateProcess(instance)).ReturnsAsync(updateInstanceResponse); + instanceService.Setup(i => i.GetInstance(updateInstanceResponse)).ReturnsAsync(getInstanceResponse); + Dictionary prefill = new Dictionary(); + + // Act + var result = await dispatcher.UpdateProcessAndDispatchEvents(instance, prefill, events); + + // Assert + result.Should().Be(getInstanceResponse); + instanceService.Verify(i => i.UpdateProcess(instance), Times.Once); + instanceService.Verify(i => i.GetInstance(updateInstanceResponse), Times.Once); + instanceEvent.Verify(p => p.SaveInstanceEvent(events[0], instance.Org, "test-app"), Times.Once); + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateProcessAndDispatchEvents_StartTask_data_instance_updated_and_events_sent_to_storage_and_trigger_ITask() + { + // Arrange + var instanceService = new Mock(); + var instanceEvent = new Mock(); + var taskEvents = new Mock(); + var appEvents = new Mock(); + var eventsService = new Mock(); + var appSettings = Options.Create(new AppSettings()); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Org = "ttd", + AppId = "ttd/test-app", + }; + Instance updateInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_1", + } + } + }; + Instance getInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_1", + Flow = 2 + } + } + }; + List events = new List() + { + new InstanceEvent() + { + EventType = InstanceEventType.process_StartTask.ToString(), + ProcessInfo = new() + { + CurrentTask = new() + { + ElementId = "Task_1", + AltinnTaskType = "data", + Name = "Utfylling", + Flow = 2 + } + } + } + }; + instanceService.Setup(i => i.UpdateProcess(instance)).ReturnsAsync(updateInstanceResponse); + instanceService.Setup(i => i.GetInstance(updateInstanceResponse)).ReturnsAsync(getInstanceResponse); + Dictionary prefill = new Dictionary(); + + // Act + var result = await dispatcher.UpdateProcessAndDispatchEvents(instance, prefill, events); + + // Assert + result.Should().Be(getInstanceResponse); + taskEvents.Verify(t => t.OnStartProcessTask("Task_1", instance, prefill), Times.Once); + instanceService.Verify(i => i.UpdateProcess(instance), Times.Once); + instanceService.Verify(i => i.GetInstance(updateInstanceResponse), Times.Once); + instanceEvent.Verify(p => p.SaveInstanceEvent(events[0], instance.Org, "test-app"), Times.Once); + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateProcessAndDispatchEvents_EndTask_confirmation_instance_updated_and_events_sent_to_storage_and_trigger_ITask() + { + // Arrange + var instanceService = new Mock(); + var instanceEvent = new Mock(); + var taskEvents = new Mock(); + var appEvents = new Mock(); + var eventsService = new Mock(); + var appSettings = Options.Create(new AppSettings()); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Org = "ttd", + AppId = "ttd/test-app", + }; + Instance updateInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + } + } + }; + Instance getInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + Flow = 3 + } + } + }; + List events = new List() + { + new InstanceEvent() + { + EventType = InstanceEventType.process_EndTask.ToString(), + ProcessInfo = new() + { + CurrentTask = new() + { + ElementId = "Task_2", + AltinnTaskType = "confirmation", + Name = "Bekreft", + Flow = 2 + } + } + } + }; + instanceService.Setup(i => i.UpdateProcess(instance)).ReturnsAsync(updateInstanceResponse); + instanceService.Setup(i => i.GetInstance(updateInstanceResponse)).ReturnsAsync(getInstanceResponse); + Dictionary prefill = new Dictionary(); + + // Act + var result = await dispatcher.UpdateProcessAndDispatchEvents(instance, prefill, events); + + // Assert + result.Should().Be(getInstanceResponse); + taskEvents.Verify(t => t.OnEndProcessTask("Task_2", instance), Times.Once); + instanceService.Verify(i => i.UpdateProcess(instance), Times.Once); + instanceService.Verify(i => i.GetInstance(updateInstanceResponse), Times.Once); + instanceEvent.Verify(p => p.SaveInstanceEvent(events[0], instance.Org, "test-app"), Times.Once); + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateProcessAndDispatchEvents_AbandonTask_feedback_instance_updated_and_events_sent_to_storage_and_trigger_ITask() + { + // Arrange + var instanceService = new Mock(); + var instanceEvent = new Mock(); + var taskEvents = new Mock(); + var appEvents = new Mock(); + var eventsService = new Mock(); + var appSettings = Options.Create(new AppSettings()); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Org = "ttd", + AppId = "ttd/test-app", + }; + Instance updateInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + } + } + }; + Instance getInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + Flow = 4 + } + } + }; + List events = new List() + { + new InstanceEvent() + { + EventType = InstanceEventType.process_AbandonTask.ToString(), + ProcessInfo = new() + { + CurrentTask = new() + { + ElementId = "Task_2", + AltinnTaskType = "feedback", + Name = "Bekreft", + Flow = 4 + } + } + } + }; + instanceService.Setup(i => i.UpdateProcess(instance)).ReturnsAsync(updateInstanceResponse); + instanceService.Setup(i => i.GetInstance(updateInstanceResponse)).ReturnsAsync(getInstanceResponse); + Dictionary prefill = new Dictionary(); + + // Act + var result = await dispatcher.UpdateProcessAndDispatchEvents(instance, prefill, events); + + // Assert + result.Should().Be(getInstanceResponse); + taskEvents.Verify(t => t.OnAbandonProcessTask("Task_2", instance), Times.Once); + instanceService.Verify(i => i.UpdateProcess(instance), Times.Once); + instanceService.Verify(i => i.GetInstance(updateInstanceResponse), Times.Once); + instanceEvent.Verify(p => p.SaveInstanceEvent(events[0], instance.Org, "test-app"), Times.Once); + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateProcessAndDispatchEvents_EndEvent_confirmation_instance_updated_and_events_sent_to_storage_and_trigger_ITask() + { + // Arrange + var instanceService = new Mock(); + var instanceEvent = new Mock(); + var taskEvents = new Mock(); + var appEvents = new Mock(); + var eventsService = new Mock(); + var appSettings = Options.Create(new AppSettings()); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Org = "ttd", + AppId = "ttd/test-app", + }; + Instance updateInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + } + } + }; + Instance getInstanceResponse = new Instance() + { + Id = instance.Id, + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + Flow = 3 + } + } + }; + List events = new List() + { + new InstanceEvent() + { + EventType = InstanceEventType.process_EndEvent.ToString(), + ProcessInfo = new() + { + CurrentTask = new() + { + ElementId = "Task_2", + AltinnTaskType = "confirmation", + Name = "Bekreft", + Flow = 2 + }, + EndEvent = "EndEvent" + } + } + }; + instanceService.Setup(i => i.UpdateProcess(instance)).ReturnsAsync(updateInstanceResponse); + instanceService.Setup(i => i.GetInstance(updateInstanceResponse)).ReturnsAsync(getInstanceResponse); + Dictionary prefill = new Dictionary(); + + // Act + var result = await dispatcher.UpdateProcessAndDispatchEvents(instance, prefill, events); + + // Assert + result.Should().Be(getInstanceResponse); + appEvents.Verify(a => a.OnEndAppEvent("EndEvent", instance), Times.Once); + instanceService.Verify(i => i.UpdateProcess(instance), Times.Once); + instanceService.Verify(i => i.GetInstance(updateInstanceResponse), Times.Once); + instanceEvent.Verify(p => p.SaveInstanceEvent(events[0], instance.Org, "test-app"), Times.Once); + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateProcessAndDispatchEvents_EndEvent_confirmation_instance_updated_and_dispatches_no_events_when_events_null() + { + // Arrange + var instanceService = new Mock(); + var instanceEvent = new Mock(); + var taskEvents = new Mock(); + var appEvents = new Mock(); + var eventsService = new Mock(); + var appSettings = Options.Create(new AppSettings()); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Org = "ttd", + AppId = "ttd/test-app", + }; + Instance updateInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + } + } + }; + Instance getInstanceResponse = new Instance() + { + Id = instance.Id, + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + Flow = 3 + } + } + }; + List events = null; + instanceService.Setup(i => i.UpdateProcess(instance)).ReturnsAsync(updateInstanceResponse); + instanceService.Setup(i => i.GetInstance(updateInstanceResponse)).ReturnsAsync(getInstanceResponse); + Dictionary prefill = new Dictionary(); + + // Act + var result = await dispatcher.UpdateProcessAndDispatchEvents(instance, prefill, events); + + // Assert + result.Should().Be(getInstanceResponse); + instanceService.Verify(i => i.UpdateProcess(instance), Times.Once); + instanceService.Verify(i => i.GetInstance(updateInstanceResponse), Times.Once); + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task RegisterEventWithEventsComponent_sends_movedTo_event_to_events_system_when_enabled_and_current_task_set() + { + // Arrange + var instanceService = new Mock(); + var instanceEvent = new Mock(); + var taskEvents = new Mock(); + var appEvents = new Mock(); + var eventsService = new Mock(); + var appSettings = Options.Create(new AppSettings() + { + RegisterEventsWithEventsComponent = true + }); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }; + + // Act + await dispatcher.RegisterEventWithEventsComponent(instance); + + // Assert + eventsService.Verify(e => e.AddEvent("app.instance.process.movedTo.Task_1", instance), Times.Once); + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task RegisterEventWithEventsComponent_sends_complete_event_to_events_system_when_currentTask_null_and_endevent_set() + { + // Arrange + var instanceService = new Mock(); + var instanceEvent = new Mock(); + var taskEvents = new Mock(); + var appEvents = new Mock(); + var eventsService = new Mock(); + var appSettings = Options.Create(new AppSettings() + { + RegisterEventsWithEventsComponent = true + }); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Process = new() + { + CurrentTask = null, + EndEvent = "EndEvent" + } + }; + + // Act + await dispatcher.RegisterEventWithEventsComponent(instance); + + // Assert + eventsService.Verify(e => e.AddEvent("app.instance.process.completed", instance), Times.Once); + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task RegisterEventWithEventsComponent_sends_no_events_when_process_is_null() + { + // Arrange + var instanceService = new Mock(); + var instanceEvent = new Mock(); + var taskEvents = new Mock(); + var appEvents = new Mock(); + var eventsService = new Mock(); + var appSettings = Options.Create(new AppSettings() + { + RegisterEventsWithEventsComponent = true + }); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Process = null + }; + + // Act + await dispatcher.RegisterEventWithEventsComponent(instance); + + // Assert + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task RegisterEventWithEventsComponent_sends_no_events_when_current_and_endevent_is_null() + { + // Arrange + var instanceService = new Mock(); + var instanceEvent = new Mock(); + var taskEvents = new Mock(); + var appEvents = new Mock(); + var eventsService = new Mock(); + var appSettings = Options.Create(new AppSettings() + { + RegisterEventsWithEventsComponent = true + }); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Process = new() + }; + + // Act + await dispatcher.RegisterEventWithEventsComponent(instance); + + // Assert + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task RegisterEventWithEventsComponent_sends_no_events_when_registereventswitheventscomponent_false() + { + // Arrange + var instanceService = new Mock(); + var instanceEvent = new Mock(); + var taskEvents = new Mock(); + var appEvents = new Mock(); + var eventsService = new Mock(); + var appSettings = Options.Create(new AppSettings() + { + RegisterEventsWithEventsComponent = false + }); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }; + + // Act + await dispatcher.RegisterEventWithEventsComponent(instance); + + // Assert + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs new file mode 100644 index 000000000..587fbed58 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs @@ -0,0 +1,188 @@ +using Altinn.App.Core.Features; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Internal.Process.Elements.Base; +using Altinn.App.Core.Tests.Internal.Process.TestUtils; +using Altinn.App.PlatformServices.Tests.Internal.Process.StubGatewayFilters; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process; + +public class ProcessNavigatorTests +{ + [Fact] + public async void GetNextTask_returns_next_element_if_no_gateway() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-linear.bpmn", new List()); + ProcessElement nextElements = await processNavigator.GetNextTask(new Instance(), "Task1", null); + nextElements.Should().BeEquivalentTo(new ProcessTask() + { + Id = "Task2", + Name = "Bekreft skjemadata", + Incoming = new List { "Flow2" }, + Outgoing = new List { "Flow3" }, + ExtensionElements = new ExtensionElements() + { + TaskExtension = new() + { + TaskType = "confirmation", + AltinnActions = new() + }, + } + }); + } + + [Fact] + public async void NextFollowAndFilterGateways_returns_empty_list_if_no_outgoing_flows() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-linear.bpmn", new List()); + ProcessElement nextElements = await processNavigator.GetNextTask(new Instance(), "EndEvent", null); + nextElements.Should().BeNull(); + } + + [Fact] + public async void GetNextTask_returns_default_if_no_filtering_is_implemented_and_default_set() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-gateway-default.bpmn", new List()); + ProcessElement nextElements = await processNavigator.GetNextTask(new Instance(), "Task1", null); + nextElements.Should().BeEquivalentTo(new ProcessTask() + { + Id = "Task2", + Name = null!, + ExtensionElements = new() + { + TaskExtension = new() + { + TaskType = "confirm", + AltinnActions = new() + { + new("confirm"), + new("reject") + } + } + }, + Incoming = new List { "Flow3" }, + Outgoing = new List { "Flow5" } + }); + } + + [Fact] + public async void GetNextTask_runs_custom_filter_and_returns_result() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-gateway-with-join-gateway.bpmn", new List() + { + new DataValuesFilter("Gateway1", "choose") + }); + Instance i = new Instance() + { + DataValues = new Dictionary() + { + { "choose", "Flow3" } + } + }; + + ProcessElement nextElements = await processNavigator.GetNextTask(i, "Task1", null); + nextElements.Should().BeEquivalentTo(new ProcessTask() + { + Id = "Task2", + Name = null!, + ExtensionElements = new() + { + TaskExtension = new() + { + TaskType = "data", + AltinnActions = new() + { + new("submit") + } + } + }, + Incoming = new List { "Flow3" }, + Outgoing = new List { "Flow5" } + }); + } + + [Fact] + public async void GetNextTask_throws_ProcessException_if_multiple_targets_found() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-gateway-with-join-gateway.bpmn", new List() + { + new DataValuesFilter("Foobar", "choose") + }); + Instance i = new Instance() + { + DataValues = new Dictionary() + { + { "choose", "Flow3" } + } + }; + + var result = await Assert.ThrowsAsync(async () => await processNavigator.GetNextTask(i, "Task1", null)); + result.Message.Should().Be("Multiple next elements found from Task1. Please supply action and filters or define a default flow."); + } + + [Fact] + public async void GetNextTask_follows_downstream_gateways() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-gateway-with-join-gateway.bpmn", new List() + { + new DataValuesFilter("Gateway1", "choose1") + }); + Instance i = new Instance() + { + DataValues = new Dictionary() + { + { "choose1", "Flow4" } + } + }; + ProcessElement nextElements = await processNavigator.GetNextTask(i, "Task1", null); + nextElements.Should().BeEquivalentTo(new EndEvent() + { + Id = "EndEvent", + Name = null!, + Incoming = new List { "Flow6" }, + Outgoing = new List() + }); + } + + [Fact] + public async void GetNextTask_runs_custom_filter_and_returns_empty_list_if_all_filtered_out() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-gateway-with-join-gateway.bpmn", new List() + { + new DataValuesFilter("Gateway1", "choose1"), + new DataValuesFilter("Gateway2", "choose2") + }); + Instance i = new Instance() + { + DataValues = new Dictionary() + { + { "choose1", "Flow4" }, + { "choose2", "Bar" } + } + }; + + ProcessElement nextElements = await processNavigator.GetNextTask(i, "Task1", null); + nextElements.Should().BeNull(); + } + + [Fact] + public async void GetNextTask_returns_empty_list_if_element_has_no_next() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-gateway-with-join-gateway.bpmn", new List()); + Instance i = new Instance(); + + ProcessElement nextElements = await processNavigator.GetNextTask(i, "EndEvent", null); + nextElements.Should().BeNull(); + } + + private static IProcessNavigator SetupProcessNavigator(string bpmnfile, IEnumerable gatewayFilters) + { + ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); + return new ProcessNavigator(pr, new ExclusiveGatewayFactory(gatewayFilters), new NullLogger()); + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs index befdcb789..e13cb4504 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs @@ -1,14 +1,13 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Linq; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; -using Altinn.App.PlatformServices.Tests.Internal.Process.TestUtils; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Internal.Process.Elements.Base; +using Altinn.App.Core.Tests.Internal.Process.TestUtils; using FluentAssertions; using Xunit; -namespace Altinn.App.PlatformServices.Tests.Internal.Process; +namespace Altinn.App.Core.Tests.Internal.Process; public class ProcessReaderTests { @@ -40,7 +39,7 @@ public void IsStartEvent_returns_false_when_element_is_not_StartEvent() pr.IsStartEvent("Foobar").Should().BeFalse(); pr.IsStartEvent(null).Should().BeFalse(); } - + [Fact] public void IsProcessTask_returns_true_when_element_is_ProcessTask() { @@ -58,7 +57,7 @@ public void IsProcessTask_returns_false_when_element_is_not_ProcessTask() pr.IsProcessTask("Foobar").Should().BeFalse(); pr.IsProcessTask(null).Should().BeFalse(); } - + [Fact] public void IsEndEvent_returns_true_when_element_is_EndEvent() { @@ -76,14 +75,14 @@ public void IsEndEvent_returns_false_when_element_is_not_EndEvent() pr.IsEndEvent("Foobar").Should().BeFalse(); pr.IsEndEvent(null).Should().BeFalse(); } - + [Fact] public void GetNextElement_returns_gateway() { var currentElement = "Task1"; ProcessReader pr = ProcessTestUtils.SetupProcessReader("simple-gateway.bpmn"); - List nextElements = pr.GetNextElementIds(currentElement); - nextElements.Should().Equal("Gateway1"); + List nextElements = pr.GetNextElements(currentElement); + nextElements.Should().BeEquivalentTo(new List() { new ExclusiveGateway() { Id = "Gateway1", Incoming = new List() { "Flow2" }, Outgoing = new List() { "Flow3", "Flow4" } } }); } [Fact] @@ -92,8 +91,18 @@ public void GetNextElement_returns_task() var bpmnfile = "simple-linear.bpmn"; var currentElement = "Task1"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - List nextElements = pr.GetNextElementIds(currentElement); - nextElements.Should().Equal("Task2"); + List nextElements = pr.GetNextElements(currentElement); + nextElements.Should().BeEquivalentTo( + new List + { + new ProcessTask() + { + Id = "Task2", + Incoming = new List { "Flow2" }, + Outgoing = new List { "Flow3" }, + Name = "Bekreft skjemadata", + } + }); } [Fact] @@ -102,131 +111,109 @@ public void GetNextElement_returns_all_targets_after_gateway() var bpmnfile = "simple-gateway.bpmn"; var currentElement = "Gateway1"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - List nextElements = pr.GetNextElementIds(currentElement); - nextElements.Should().Equal("Task2", "EndEvent"); + List nextElements = pr.GetNextElements(currentElement); + nextElements.Should().BeEquivalentTo( + new List + { + new ProcessTask() + { + Id = "Task2", + Incoming = new List() { "Flow3" }, + Outgoing = new List() { "Flow5" }, + }, + new EndEvent() + { + Id = "EndEvent", + Incoming = new List() { "Flow4", "Flow5" }, + Outgoing = new List() + } + }); } - + [Fact] public void GetNextElement_returns_task1_in_simple_process() { var bpmnfile = "simple-linear.bpmn"; var currentElement = "StartEvent"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - List nextElements = pr.GetNextElementIds(currentElement); - nextElements.Should().Equal("Task1"); + List nextElements = pr.GetNextElements(currentElement); + nextElements.Should().BeEquivalentTo( + new List() + { + new ProcessTask() + { + Id = "Task1", + Name = "Utfylling", + Incoming = new List() { "Flow1" }, + Outgoing = new List() { "Flow2" }, + } + }); } - + [Fact] public void GetNextElement_returns_task2_in_simple_process() { var bpmnfile = "simple-linear.bpmn"; var currentElement = "Task1"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - List nextElements = pr.GetNextElementIds(currentElement); - nextElements.Should().Equal("Task2"); + List nextElements = pr.GetNextElements(currentElement); + nextElements.Should().BeEquivalentTo( + new List() + { + new ProcessTask() + { + Id = "Task2", + Name = "Bekreft skjemadata", + Incoming = new List() { "Flow2" }, + Outgoing = new List() { "Flow3" }, + } + }); } - + [Fact] public void GetNextElement_returns_endevent_in_simple_process() { var bpmnfile = "simple-linear.bpmn"; var currentElement = "Task2"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - List nextElements = pr.GetNextElementIds(currentElement); - nextElements.Should().Equal("EndEvent"); + List nextElements = pr.GetNextElements(currentElement); + nextElements.Should().BeEquivalentTo( + new List() + { + new EndEvent() + { + Id = "EndEvent", + Incoming = new List() { "Flow3" }, + Outgoing = new List() + } + }); } - + [Fact] public void GetNextElement_returns_emptylist_if_task_without_output() { var bpmnfile = "simple-no-end.bpmn"; var currentElement = "Task2"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - List nextElements = pr.GetNextElementIds(currentElement); + List nextElements = pr.GetNextElements(currentElement); nextElements.Should().HaveCount(0); } - + [Fact] public void GetNextElement_currentElement_null() { var bpmnfile = "simple-linear.bpmn"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - pr.Invoking(p => p.GetNextElementIds(null!)).Should().Throw(); + pr.Invoking(p => p.GetNextElements(null!)).Should().Throw(); } - + [Fact] public void GetNextElement_throws_exception_if_step_not_found() { var bpmnfile = "simple-linear.bpmn"; var currentElement = "NoStep"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - pr.Invoking(p => p.GetNextElementIds(currentElement)).Should().Throw(); - } - - [Fact] - public void GetElementInfo_returns_correct_info_for_ProcessTask() - { - var bpmnfile = "simple-linear.bpmn"; - var currentElement = "Task1"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetElementInfo(currentElement); - actual.Should().BeEquivalentTo(new ElementInfo() - { - Id = "Task1", - Name = "Utfylling", - AltinnTaskType = "data", - ElementType = "Task" - }); - } - - [Fact] - public void GetElementInfo_returns_correct_info_for_StartEvent() - { - var bpmnfile = "simple-gateway-default.bpmn"; - var currentElement = "StartEvent"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetElementInfo(currentElement); - actual.Should().BeEquivalentTo(new ElementInfo() - { - Id = "StartEvent", - Name = null!, - AltinnTaskType = null!, - ElementType = "StartEvent" - }); - } - - [Fact] - public void GetElementInfo_returns_correct_info_for_EndEvent() - { - var bpmnfile = "simple-gateway-default.bpmn"; - var currentElement = "EndEvent"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetElementInfo(currentElement); - actual.Should().BeEquivalentTo(new ElementInfo() - { - Id = "EndEvent", - Name = null!, - AltinnTaskType = null!, - ElementType = "EndEvent" - }); - } - - [Fact] - public void GetElementInfo_returns_null_for_ExclusiveGateway() - { - var bpmnfile = "simple-gateway-default.bpmn"; - var currentElement = "Gateway1"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetElementInfo(currentElement); - actual.Should().BeNull(); - } - - [Fact] - public void GetElementInfo_throws_argument_null_expcetion_when_elementName_is_null() - { - var bpmnfile = "simple-linear.bpmn"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - pr.Invoking(p => p.GetElementInfo(null!)).Should().Throw(); + pr.Invoking(p => p.GetNextElements(currentElement)).Should().Throw(); } [Fact] @@ -254,7 +241,7 @@ public void GetOutgoingSequenceFlows_returns_SequenceFlow_objects_for_outgoing_f } }); } - + [Fact] public void GetOutgoingSequenceFlows_returns_SequenceFlow_objects_for_outgoing_flows_from_Gateway() { @@ -279,7 +266,7 @@ public void GetOutgoingSequenceFlows_returns_SequenceFlow_objects_for_outgoing_f } }); } - + [Fact] public void GetOutgoingSequenceFlows_returns_empty_list_when_no_outgoing() { @@ -288,102 +275,6 @@ public void GetOutgoingSequenceFlows_returns_empty_list_when_no_outgoing() List outgoingFLows = pr.GetOutgoingSequenceFlows(pr.GetFlowElement("EndEvent")); outgoingFLows.Should().BeEmpty(); } - - [Fact] - public void GetSequenceFlowsBetween_returns_all_sequenceflows_between_StartEvent_and_Task1() - { - var bpmnfile = "simple-gateway-default.bpmn"; - var currentElement = "StartEvent"; - var nextElementId = "Task1"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEquivalentTo("Flow1"); - } - - [Fact] - public void GetSequenceFlowsBetween_returns_all_sequenceflows_between_Task1_and_Task2() - { - var bpmnfile = "simple-gateway-default.bpmn"; - var currentElement = "Task1"; - var nextElementId = "Task2"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEquivalentTo("Flow2", "Flow3"); - } - - [Fact] - public void GetSequenceFlowsBetween_returns_all_sequenceflows_between_Task1_and_EndEvent() - { - var bpmnfile = "simple-gateway-default.bpmn"; - var currentElement = "Task1"; - var nextElementId = "EndEvent"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEquivalentTo("Flow2", "Flow4"); - } - - [Fact] - public void GetSequenceFlowsBetween_returns_all_sequenceflows_between_Task1_and_EndEvent_complex() - { - var bpmnfile = "simple-gateway-with-join-gateway.bpmn"; - var currentElement = "Task1"; - var nextElementId = "EndEvent"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEquivalentTo("Flow2", "Flow4", "Flow6"); - } - - [Fact] - public void GetSequenceFlowsBetween_returns_empty_list_when_unknown_target() - { - var bpmnfile = "simple-gateway-default.bpmn"; - var currentElement = "Task1"; - var nextElementId = "Foobar"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEmpty(); - } - - [Fact] - public void GetSequenceFlowsBetween_returns_empty_list_when_current_is_null() - { - var bpmnfile = "simple-gateway-default.bpmn"; - string? currentElement = null; - var nextElementId = "Foobar"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEmpty(); - } - - [Fact] - public void GetSequenceFlowsBetween_returns_empty_list_when_next_is_null() - { - var bpmnfile = "simple-gateway-default.bpmn"; - string currentElement = "Task1"; - string? nextElementId = null; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEmpty(); - } - - [Fact] - public void GetSequenceFlowsBetween_returns_empty_list_when_current_and_next_is_null() - { - var bpmnfile = "simple-gateway-default.bpmn"; - string? currentElement = null; - string? nextElementId = null; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEmpty(); - } [Fact] public void Constructor_Fails_if_invalid_bpmn() @@ -404,7 +295,7 @@ public void GetFlowElement_returns_StartEvent_with_id() Outgoing = new List { "Flow1" } }); } - + [Fact] public void GetFlowElement_returns_ProcessTask_with_id() { @@ -415,10 +306,35 @@ public void GetFlowElement_returns_ProcessTask_with_id() Id = "Task1", Name = null!, Incoming = new List { "Flow1" }, - Outgoing = new List { "Flow2" } + Outgoing = new List { "Flow2" }, + ExtensionElements = new ExtensionElements() + { + TaskExtension = new AltinnTaskExtension() + { + AltinnActions = new List() + { + new("submit", ActionType.ProcessAction), + new("lookup", ActionType.ServerAction) + }, + TaskType = "data", + SignatureConfiguration = new() + { + DataTypesToSign = new() + { + "default", + "default2" + }, + SignatureDataType = "signature", + UniqueFromSignaturesInDataTypes = new() + { + "signature1" + } + } + } + } }); } - + [Fact] public void GetFlowElement_returns_EndEvent_with_id() { @@ -432,7 +348,7 @@ public void GetFlowElement_returns_EndEvent_with_id() Outgoing = new List() }); } - + [Fact] public void GetFlowElement_returns_null_when_id_not_found() { @@ -440,7 +356,7 @@ public void GetFlowElement_returns_null_when_id_not_found() ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); pr.GetFlowElement("Foobar").Should().BeNull(); } - + [Fact] public void GetFlowElement_returns_Gateway_with_id() { diff --git a/test/Altinn.App.Core.Tests/Internal/Process/StubGatewayFilters/DataValuesFilter.cs b/test/Altinn.App.Core.Tests/Internal/Process/StubGatewayFilters/DataValuesFilter.cs index 9565fc8db..04709370c 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/StubGatewayFilters/DataValuesFilter.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/StubGatewayFilters/DataValuesFilter.cs @@ -1,15 +1,15 @@ -using System.Collections.Generic; -using System.Threading.Tasks; +#nullable enable using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Models.Process; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.PlatformServices.Tests.Internal.Process.StubGatewayFilters; -public class DataValuesFilter: IProcessExclusiveGateway +public class DataValuesFilter : IProcessExclusiveGateway { public string GatewayId { get; } - + private readonly string _filterOnDataValue; public DataValuesFilter(string gatewayId, string filterOnDataValue) @@ -17,8 +17,8 @@ public DataValuesFilter(string gatewayId, string filterOnDataValue) GatewayId = gatewayId; _filterOnDataValue = filterOnDataValue; } - - public async Task> FilterAsync(List outgoingFlows, Instance instance) + + public async Task> FilterAsync(List outgoingFlows, Instance instance, ProcessGatewayInformation processGatewayInformation) { var targetFlow = instance.DataValues[_filterOnDataValue]; return await Task.FromResult(outgoingFlows.FindAll(e => e.Id == targetFlow)); diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/DummyModel.cs b/test/Altinn.App.Core.Tests/Internal/Process/TestData/DummyModel.cs new file mode 100644 index 000000000..a2b5bd380 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/DummyModel.cs @@ -0,0 +1,9 @@ +namespace Altinn.App.Core.Tests.Internal.Process.TestData +{ + public class DummyModel + { + public string Submitter { get; set; } + + public decimal Amount { get; set; } + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/ProcessEngineTest/config/process/process.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/ProcessEngineTest/config/process/process.bpmn index f28219543..6c389b3bc 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/ProcessEngineTest/config/process/process.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/ProcessEngineTest/config/process/process.bpmn @@ -11,9 +11,17 @@ targetNamespace="http://bpmn.io/schema/bpmn" > SequenceFlow_1n56yn5 - + SequenceFlow_1n56yn5 SequenceFlow_1oot28q + + + + + + + + SequenceFlow_1oot28q diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn index 45af6b661..47603d6a3 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn @@ -1,27 +1,61 @@ - + Flow1 - + Flow1 Flow2 + + + + submit + lookup + + data + + + default + default2 + + signature + + signature1 + + + + - + Flow2 Flow3 Flow4 - - + + Flow3 Flow5 + + + + confirm + reject + + confirm + + - + Flow5 Flow4 diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn index 441a4aa95..054dde9a2 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn @@ -1,33 +1,55 @@ - + Flow1 - + Flow1 Flow2 + + + + submit + + data + + - + Flow2 Flow3 Flow4 - - + + Flow3 Flow5 + + + + submit + + data + + - + - Flow4 - Flow5 - Flow6 - - + Flow4 + Flow5 + Flow6 + + Flow6 diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway.bpmn index ce5be22a4..ced273995 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway.bpmn @@ -1,27 +1,50 @@ - + Flow1 - + Flow1 Flow2 + + + + submit + + data + + - + Flow2 Flow3 Flow4 - - + + Flow3 Flow5 + + + + confirm + reject + + confirm + + - + Flow5 Flow4 diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-both.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-both.bpmn new file mode 100644 index 000000000..f23ed4872 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-both.bpmn @@ -0,0 +1,46 @@ + + + + + Flow1 + + + + Flow1 + Flow2 + + + + submit + + data2 + + + + + + Flow2 + Flow3 + + + + confirm + reject + + confirmation2 + + + + + + Flow3 + + + diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-new.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-new.bpmn new file mode 100644 index 000000000..599f7d91a --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-new.bpmn @@ -0,0 +1,46 @@ + + + + + Flow1 + + + + Flow1 + Flow2 + + + data + + submit + + + + + + + Flow2 + Flow3 + + + + confirm + reject + + confirmation + + + + + + Flow3 + + + diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear.bpmn index f2db44b72..f28a30879 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear.bpmn @@ -6,20 +6,30 @@ xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" targetNamespace="http://bpmn.io/schema/bpmn" -xmlns:altinn="http://altinn.no"> +xmlns:altinn="http://altinn.no/process"> Flow1 - + Flow1 Flow2 + + + data + + - + Flow2 Flow3 + + + confirmation + + diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-no-end.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-no-end.bpmn index 94cc8a644..66b353c33 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-no-end.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-no-end.bpmn @@ -12,13 +12,23 @@ xmlns:altinn="http://altinn.no"> Flow1 - + Flow1 Flow2 + + + data + + - + Flow2 + + + confirmation + + diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs b/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs index a0483d88a..2816d6a40 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs @@ -1,18 +1,22 @@ -using System.IO; -using Altinn.App.Core.Interface; +#nullable enable using Altinn.App.Core.Internal.Process; using Moq; -namespace Altinn.App.PlatformServices.Tests.Internal.Process.TestUtils; +namespace Altinn.App.Core.Tests.Internal.Process.TestUtils; internal static class ProcessTestUtils { private static readonly string TestDataPath = Path.Combine("Internal", "Process", "TestData"); - - internal static ProcessReader SetupProcessReader(string bpmnfile) + + internal static ProcessReader SetupProcessReader(string bpmnfile, string? testDataPath = null) { - Mock processServiceMock = new Mock(); - var s = new FileStream(Path.Combine(TestDataPath, bpmnfile), FileMode.Open, FileAccess.Read); + if (testDataPath == null) + { + testDataPath = TestDataPath; + } + + Mock processServiceMock = new Mock(); + var s = new FileStream(Path.Combine(testDataPath, bpmnfile), FileMode.Open, FileAccess.Read); processServiceMock.Setup(p => p.GetProcessDefinition()).Returns(s); return new ProcessReader(processServiceMock.Object); } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ContextListRoot.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ContextListRoot.cs index 2fdf9a04b..716476377 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ContextListRoot.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ContextListRoot.cs @@ -1,5 +1,6 @@ #nullable enable using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Altinn.App.Core.Models.Layout; @@ -31,7 +32,7 @@ public class ContextListRoot public LayoutModel ComponentModel { get; set; } = default!; [JsonPropertyName("dataModel")] - public JsonElement? DataModel { get; set; } + public JsonObject? DataModel { get; set; } [JsonPropertyName("expectedContexts")] public List Expected { get; set; } = default!; @@ -40,4 +41,4 @@ public override string ToString() { return $"{Name} ({Folder}/{Filename})"; } -} \ No newline at end of file +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs index f8842afc7..a7ca45c77 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs @@ -1,5 +1,6 @@ #nullable enable using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Altinn.App.Core.Configuration; @@ -46,13 +47,16 @@ public class ExpressionTestCaseRoot public LayoutModel ComponentModel { get; set; } = default!; [JsonPropertyName("dataModel")] - public JsonElement? DataModel { get; set; } + public JsonObject? DataModel { get; set; } [JsonPropertyName("frontendSettings")] public FrontEndSettings? FrontEndSettings { get; set; } [JsonPropertyName("instance")] public Instance? Instance { get; set; } + + [JsonPropertyName("gatewayAction")] + public string? GatewayAction { get; set; } public override string ToString() { diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs new file mode 100644 index 000000000..66833b0d4 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs @@ -0,0 +1,140 @@ +#nullable enable +using System.Reflection; +using System.Text.Json; + +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Tests.Helpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Altinn.App.Core.Tests.LayoutExpressions; + +public class TestBackendExclusiveFunctions +{ + private readonly ITestOutputHelper _output; + + public TestBackendExclusiveFunctions(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [ExclusiveTest("gatewayAction")] + public void GatewayAction_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); + + private void RunTestCase(ExpressionTestCaseRoot test) + { + _output.WriteLine($"{test.Filename} in {test.Folder}"); + _output.WriteLine(test.RawJson); + _output.WriteLine(test.FullPath); + var state = new LayoutEvaluatorState( + new JsonDataModel(test.DataModel), + test.ComponentModel, + test.FrontEndSettings ?? new(), + test.Instance ?? new(), + test.GatewayAction); + + if (test.ExpectsFailure is not null) + { + if (test.ParsingException is not null) + { + test.ParsingException.Message.Should().Be(test.ExpectsFailure); + } + else + { + Action act = () => + { + ExpressionEvaluator.EvaluateExpression(state, test.Expression, test.Context?.ToContext(test.ComponentModel)!); + }; + act.Should().Throw().WithMessage(test.ExpectsFailure); + } + + return; + } + + test.ParsingException.Should().BeNull("Loading of test failed"); + + var result = ExpressionEvaluator.EvaluateExpression(state, test.Expression, test.Context?.ToContext(test.ComponentModel)!); + + switch (test.Expects.ValueKind) + { + case JsonValueKind.String: + result.Should().Be(test.Expects.GetString()); + break; + case JsonValueKind.True: + result.Should().Be(true); + break; + case JsonValueKind.False: + result.Should().Be(false); + break; + case JsonValueKind.Null: + result.Should().Be(null); + break; + case JsonValueKind.Number: + result.Should().Be(test.Expects.GetDouble()); + break; + case JsonValueKind.Undefined: + + default: + // Compare serialized json result for object and array + JsonSerializer.Serialize(result).Should().Be(JsonSerializer.Serialize(test.Expects)); + break; + } + } + + [Fact] + public void Ensure_tests_For_All_Folders() + { + // This is just a way to ensure that all folders have test methods associcated. + var jsonTestFolders = Directory.GetDirectories(Path.Join("LayoutExpressions", "CommonTests", "exclusive-tests", "functions")).Select(d => Path.GetFileName(d)).ToArray(); + var testMethods = this.GetType().GetMethods().Select(m => m.CustomAttributes.FirstOrDefault(ca => ca.AttributeType == typeof(ExclusiveTestAttribute))?.ConstructorArguments.FirstOrDefault().Value).OfType().ToArray(); + testMethods.Should().BeEquivalentTo(jsonTestFolders, "Shared test folders should have a corresponding test method"); + } +} + +public class ExclusiveTestAttribute : DataAttribute +{ + private readonly string _folder; + + public ExclusiveTestAttribute(string folder) + { + _folder = folder; + } + + public override IEnumerable GetData(MethodInfo methodInfo) + { + var files = Directory.GetFiles(Path.Join("LayoutExpressions", "CommonTests", "exclusive-tests", "functions", _folder)); + foreach (var file in files) + { + ExpressionTestCaseRoot testCase = new(); + var data = File.ReadAllText(file); + try + { + testCase = JsonSerializer.Deserialize( + data, + new JsonSerializerOptions + { + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + })!; + } + catch (Exception e) + { + using var jsonDocument = JsonDocument.Parse(data); + + testCase.Name = jsonDocument.RootElement.GetProperty("name").GetString(); + testCase.ExpectsFailure = jsonDocument.RootElement.TryGetProperty("expectsFailure", out var expectsFailure) ? expectsFailure.GetString() : null; + testCase.ParsingException = e; + } + + testCase.Filename = Path.GetFileName(file); + testCase.FullPath = file; + testCase.Folder = _folder; + testCase.RawJson = data; + + yield return new object[] { testCase }; + } + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs index 695ac6812..3fbd7722e 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs @@ -95,7 +95,7 @@ public TestFunctions(ITestOutputHelper output) [Theory] [SharedTest("or")] public void Or_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); - + [Theory] [SharedTest("unknown")] public void Unknown_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/no-action-defined-is-null.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/no-action-defined-is-null.json new file mode 100644 index 000000000..a172bd2bb --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/no-action-defined-is-null.json @@ -0,0 +1,22 @@ +{ + "name": "Simple lookup", + "expression": ["gatewayAction"], + "expects": null, + "layouts": { + "Page1": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "current-component", + "type": "Paragraph" + } + ] + } + } + }, + "context": { + "component": "current-component", + "currentLayout": "Page1" + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/simple-lookup-equals.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/simple-lookup-equals.json new file mode 100644 index 000000000..625a6539d --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/simple-lookup-equals.json @@ -0,0 +1,23 @@ +{ + "name": "Simple lookup", + "expression": ["equals",["gatewayAction"], "sign"], + "expects": true, + "gatewayAction": "sign", + "layouts": { + "Page1": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "current-component", + "type": "Paragraph" + } + ] + } + } + }, + "context": { + "component": "current-component", + "currentLayout": "Page1" + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/simple-lookup.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/simple-lookup.json new file mode 100644 index 000000000..f50b1dc38 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/simple-lookup.json @@ -0,0 +1,23 @@ +{ + "name": "Simple lookup", + "expression": ["gatewayAction"], + "expects": "sign", + "gatewayAction": "sign", + "layouts": { + "Page1": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "current-component", + "type": "Paragraph" + } + ] + } + } + }, + "context": { + "component": "current-component", + "currentLayout": "Page1" + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/noData.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/noData.json index 1596105a1..551089fc2 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/noData.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/noData.json @@ -10,12 +10,11 @@ }, { "id": "group1", - "type": "Group", + "type": "RepeatingGroup", "children": ["comp3", "comp4"], "dataModelBindings": { "group": "dddd" - }, - "maxCount": 99 + } }, { "id": "comp3", diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/oneRow.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/oneRow.json index b043d9411..5d2852974 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/oneRow.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/oneRow.json @@ -10,11 +10,10 @@ }, { "id": "group1", - "type": "Group", + "type": "RepeatingGroup", "dataModelBindings": { "group": "gruppe1" }, - "maxCount": 99, "children": ["comp3", "comp4"] }, { diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/twoRows.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/twoRows.json index 89dd4f259..0cfecf2d4 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/twoRows.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/twoRows.json @@ -10,11 +10,10 @@ }, { "id": "group1", - "type": "Group", + "type": "RepeatingGroup", "dataModelBindings": { "group": "gruppe1" }, - "maxCount": 99, "children": ["comp3", "comp4"] }, { diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount0.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount0.json deleted file mode 100644 index 8c038be56..000000000 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount0.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "name": "Non-repeating group with maxCount = 0", - "layouts": { - "Page1": { - "data": { - "layout": [ - { - "id": "comp1", - "type": "Header" - }, - { - "id": "group1", - "type": "Group", - "children": ["comp3", "comp4"], - "maxCount": 0 - }, - { - "id": "comp3", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - }, - { - "id": "comp4", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - } - ] - } - }, - "Page2": { - "data": { - "layout": [ - { - "id": "comp5", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - }, - { - "id": "comp6", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - } - ] - } - } - }, - "expectedContexts": [ - { - "component": "Page1", - "currentLayout": "Page1", - "children": [ - { - "component": "comp1", - "currentLayout": "Page1" - }, - { - "component": "group1", - "currentLayout": "Page1", - "children": [ - { - "component": "comp3", - "currentLayout": "Page1" - }, - { - "component": "comp4", - "currentLayout": "Page1" - } - ] - } - ] - }, - { - "component": "Page2", - "currentLayout": "Page2", - "children": [ - { - "component": "comp5", - "currentLayout": "Page2" - }, - { - "component": "comp6", - "currentLayout": "Page2" - } - ] - } - ], - "dataModel": {} -} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount1.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount1.json deleted file mode 100644 index 7019c6eed..000000000 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount1.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "name": "Non-repeating group with maxCount = 1", - "layouts": { - "Page1": { - "data": { - "layout": [ - { - "id": "comp1", - "type": "Header" - }, - { - "id": "group1", - "type": "Group", - "children": ["comp3", "comp4"], - "maxCount": 1 - }, - { - "id": "comp3", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - }, - { - "id": "comp4", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - } - ] - } - }, - "Page2": { - "data": { - "layout": [ - { - "id": "comp5", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - }, - { - "id": "comp6", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - } - ] - } - } - }, - "expectedContexts": [ - { - "component": "Page1", - "currentLayout": "Page1", - "children": [ - { - "component": "comp1", - "currentLayout": "Page1" - }, - { - "component": "group1", - "currentLayout": "Page1", - "children": [ - { - "component": "comp3", - "currentLayout": "Page1" - }, - { - "component": "comp4", - "currentLayout": "Page1" - } - ] - } - ] - }, - { - "component": "Page2", - "currentLayout": "Page2", - "children": [ - { - "component": "comp5", - "currentLayout": "Page2" - }, - { - "component": "comp6", - "currentLayout": "Page2" - } - ] - } - ], - "dataModel": {} -} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/simple.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/simple.json index 56d2c9252..7ea533dd1 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/simple.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/simple.json @@ -1,5 +1,5 @@ { - "name": "Non-repeating group with no maxCount", + "name": "Non-repeating group", "layouts": { "Page1": { "data": { diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveNoData.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveNoData.json index 05ff77bc9..13f303325 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveNoData.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveNoData.json @@ -10,18 +10,16 @@ }, { "id": "group0", - "type": "Group", + "type": "RepeatingGroup", "children": ["group1"], - "maxCount": 99, "dataModelBindings": { "group": "group" } }, { "id": "group1", - "type": "Group", + "type": "RepeatingGroup", "children": ["comp3", "comp4"], - "maxCount": 99, "dataModelBindings": { "group": "group.subgroup" } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveOneRow.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveOneRow.json index a0329b437..c51dbdcc2 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveOneRow.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveOneRow.json @@ -10,18 +10,16 @@ }, { "id": "group0", - "type": "Group", + "type": "RepeatingGroup", "children": ["group1"], - "maxCount": 99, "dataModelBindings": { "group": "group" } }, { "id": "group1", - "type": "Group", + "type": "RepeatingGroup", "children": ["comp3", "comp4"], - "maxCount": 99, "dataModelBindings": { "group": "group.subgroup" } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsInner.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsInner.json index d4b60ccd3..19431fc67 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsInner.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsInner.json @@ -10,18 +10,16 @@ }, { "id": "group0", - "type": "Group", + "type": "RepeatingGroup", "children": ["group1"], - "maxCount": 99, "dataModelBindings": { "group": "group" } }, { "id": "group1", - "type": "Group", + "type": "RepeatingGroup", "children": ["comp3", "comp4"], - "maxCount": 99, "dataModelBindings": { "group": "group.subgroup" } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsOuter.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsOuter.json index 0bf29ee1c..847d284f3 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsOuter.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsOuter.json @@ -10,18 +10,16 @@ }, { "id": "group0", - "type": "Group", + "type": "RepeatingGroup", "children": ["group1"], - "maxCount": 99, "dataModelBindings": { "group": "group" } }, { "id": "group1", - "type": "Group", + "type": "RepeatingGroup", "children": ["comp3", "comp4"], - "maxCount": 99, "dataModelBindings": { "group": "group.subgroup" } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages-hidden.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages-hidden.json index 92cebf4ce..8f6a573d1 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages-hidden.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages-hidden.json @@ -22,8 +22,7 @@ }, { "id": "page1-Group", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Dyr" }, @@ -52,8 +51,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, @@ -75,8 +73,7 @@ }, { "id": "favoritt-dyr", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker.FavorittDyr" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages.json index 1e13499c2..719ce6824 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages.json @@ -21,8 +21,7 @@ }, { "id": "page1-Group", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Dyr" }, @@ -51,8 +50,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, @@ -74,8 +72,7 @@ }, { "id": "favoritt-dyr", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker.FavorittDyr" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-1.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-1.json index 0caa9dc70..92d5a2b44 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-1.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-1.json @@ -21,8 +21,7 @@ }, { "id": "page1-Group", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Dyr" }, @@ -51,8 +50,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, @@ -74,8 +72,7 @@ }, { "id": "favoritt-dyr", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker.FavorittDyr" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-2.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-2.json index aa6d9f0d3..200a5f4ed 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-2.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-2.json @@ -21,8 +21,7 @@ }, { "id": "page1-Group", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Dyr" }, @@ -51,8 +50,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, @@ -74,8 +72,7 @@ }, { "id": "favoritt-dyr", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker.FavorittDyr" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group-other-row.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group-other-row.json index dbdee9af8..f615857de 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group-other-row.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group-other-row.json @@ -9,8 +9,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -25,8 +24,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group.json index ec6077e15..60fc6325f 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group.json @@ -9,8 +9,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -25,8 +24,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hide-group-component.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hide-group-component.json index bde26be68..ab8b1ed77 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hide-group-component.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hide-group-component.json @@ -16,8 +16,7 @@ }, { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -32,8 +31,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-group-hidden.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-group-hidden.json index e08b0eaf9..e7f66b46b 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-group-hidden.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-group-hidden.json @@ -20,8 +20,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-page-hidden.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-page-hidden.json index 953ce3bd0..6a99c1029 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-page-hidden.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-page-hidden.json @@ -21,8 +21,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-with-hidden.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-with-hidden.json index 634a2832c..1662f9e6d 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-with-hidden.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-with-hidden.json @@ -20,8 +20,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group.json index 3125d5f7c..6a4b79505 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group.json @@ -20,8 +20,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group.json index 0fd9a9029..e2830ea76 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group.json @@ -20,8 +20,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -36,8 +35,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group2.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group2.json index dd1f258fd..d1bc4c89d 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group2.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group2.json @@ -20,8 +20,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -36,8 +35,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-group.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-group.json index a6fb981a0..d211b3a24 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-group.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-group.json @@ -20,8 +20,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group.json index f411f4af3..c5edf05c6 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group.json @@ -20,8 +20,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -36,8 +35,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group2.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group2.json index 25c2695d7..2b686075b 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group2.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group2.json @@ -20,8 +20,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -36,8 +35,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group3.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group3.json index 3e25751c6..1c3986e5b 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group3.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group3.json @@ -20,8 +20,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -36,8 +35,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group4.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group4.json index e085a88c9..67adb67ad 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group4.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group4.json @@ -20,8 +20,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -36,8 +35,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-group.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-group.json index 86c03709e..12e613432 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-group.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-group.json @@ -20,8 +20,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-nested-group.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-nested-group.json index 3ae88bd8b..44b05b56c 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-nested-group.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-nested-group.json @@ -20,8 +20,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -36,8 +35,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/failures.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/failures.json index 76925c1bf..52b268015 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/failures.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/failures.json @@ -23,8 +23,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -41,8 +40,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, @@ -103,8 +101,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -121,8 +118,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/successful.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/successful.json index a6f15d7ff..415e41a0e 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/successful.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/successful.json @@ -23,8 +23,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -41,8 +40,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, @@ -104,8 +102,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -122,8 +119,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/LayoutTestUtils.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/LayoutTestUtils.cs index 0fcebabd6..d1fdd0d61 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/LayoutTestUtils.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/LayoutTestUtils.cs @@ -3,7 +3,8 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Expressions; using Altinn.App.Core.Models.Layout; using Altinn.App.Core.Models.Layout.Components; @@ -19,9 +20,9 @@ public static async Task GetLayoutModelTools(object model, { var services = new ServiceCollection(); - var data = new Mock(); + var data = new Mock(); data.Setup(d => d.GetFormData(default, default!, default!, default!, default, default)).ReturnsAsync(model); - services.AddTransient((sp) => data.Object); + services.AddTransient((sp) => data.Object); var resources = new Mock(); var layoutModel = new LayoutModel(); diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/SecondPage.json b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/SecondPage.json index 4e6d4a07a..82a10ff86 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/SecondPage.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/SecondPage.json @@ -5,8 +5,7 @@ { "id": "firstField", "type": "Summary", - "componentRef": "gruppe1", - "pageRef": "FirstPage" + "componentRef": "gruppe1" } ] } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/SecondPage.json b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/SecondPage.json index 4e6d4a07a..82a10ff86 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/SecondPage.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/SecondPage.json @@ -5,8 +5,7 @@ { "id": "firstField", "type": "Summary", - "componentRef": "gruppe1", - "pageRef": "FirstPage" + "componentRef": "gruppe1" } ] } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs index f0b1c87ac..89bd65995 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs @@ -1,5 +1,6 @@ #nullable enable using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Altinn.App.Core.Helpers; using Altinn.App.Core.Helpers.DataModel; @@ -8,6 +9,7 @@ using Newtonsoft.Json; using Xunit; +using JsonSerializer = System.Text.Json.JsonSerializer; namespace Altinn.App.Core.Tests.LayoutExpressions.CSharpTests; @@ -96,8 +98,8 @@ public void RecursiveLookup() modelHelper.GetModelData("friends.name.value", new int[] { 1 }).Should().Be("Dolly Duck"); // Run the same tests with JsonDataModel - using var doc = JsonDocument.Parse(System.Text.Json.JsonSerializer.Serialize(model)); - modelHelper = new JsonDataModel(doc.RootElement); + var doc = JsonSerializer.Deserialize(JsonSerializer.Serialize(model)); + modelHelper = new JsonDataModel(doc); modelHelper.GetModelData("friends.name.value", default).Should().BeNull(); modelHelper.GetModelData("friends[0].name.value", default).Should().Be("Donald Duck"); modelHelper.GetModelData("friends.name.value", new int[] { 0 }).Should().Be("Donald Duck"); @@ -176,8 +178,8 @@ public void DoubleRecursiveLookup() modelHelper.GetModelDataCount("friends.friends", new int[] { 1 }).Should().Be(1); // Run the same tests with JsonDataModel - using var doc = JsonDocument.Parse(System.Text.Json.JsonSerializer.Serialize(model)); - modelHelper = new JsonDataModel(doc.RootElement); + var doc = JsonSerializer.Deserialize(JsonSerializer.Serialize(model)); + modelHelper = new JsonDataModel(doc); modelHelper.GetModelData("friends[1].friends[0].name.value", default).Should().Be("Onkel Skrue"); modelHelper.GetModelData("friends[1].friends.name.value", new int[] { 0, 0 }).Should().BeNull(); modelHelper.GetModelData("friends[1].friends.name.value", new int[] { 1, 0 }).Should().BeNull("context indexes should not be used after literal index is used"); diff --git a/test/Altinn.App.Core.Tests/TestHelpers/PrometheusTestHelper.cs b/test/Altinn.App.Core.Tests/TestHelpers/PrometheusTestHelper.cs new file mode 100644 index 000000000..71b5d93b6 --- /dev/null +++ b/test/Altinn.App.Core.Tests/TestHelpers/PrometheusTestHelper.cs @@ -0,0 +1,15 @@ +using Prometheus; + +namespace Altinn.App.Core.Tests.TestHelpers; + +public class PrometheusTestHelper +{ + public static async Task ReadPrometheusMetricsToString() + { + MemoryStream memoryStream = new MemoryStream(); + await Metrics.DefaultRegistry.CollectAndExportAsTextAsync(memoryStream); + using StreamReader reader = new StreamReader(memoryStream); + memoryStream.Position = 0; + return reader.ReadToEnd(); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/secrets.json b/test/Altinn.App.Core.Tests/secrets.json new file mode 100644 index 000000000..7e98541de --- /dev/null +++ b/test/Altinn.App.Core.Tests/secrets.json @@ -0,0 +1,3 @@ +{ + "secretId": "local secret dummy data" +} \ No newline at end of file diff --git a/test/Dockerfile b/test/Dockerfile new file mode 100644 index 000000000..c9578caa9 --- /dev/null +++ b/test/Dockerfile @@ -0,0 +1,17 @@ +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY AppLibDotnet.sln . +COPY test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj test/Altinn.App.Api.Tests/ +COPY test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj test/Altinn.App.Core.Tests/ +COPY src/Altinn.App.Api/Altinn.App.Api.csproj src/Altinn.App.Api/ +COPY src/Altinn.App.Core/Altinn.App.Core.csproj src/Altinn.App.Core/ +RUN dotnet restore + +COPY . . + +RUN dotnet test + +# Run in project root with +# docker build --progress=plain -f test/Dockerfile . \ No newline at end of file