diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 000000000..81a01270a
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,20 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "csharpier": {
+ "version": "0.27.3",
+ "commands": [
+ "dotnet-csharpier"
+ ],
+ "rollForward": false
+ },
+ "swashbuckle.aspnetcore.cli": {
+ "version": "6.6.2",
+ "commands": [
+ "swagger"
+ ],
+ "rollForward": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
index c8a06e7de..aeb256f8b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -2,6 +2,7 @@
root = true
[*]
+file_header_template = SPDX-License-Identifier: Apache-2.0\nLicensed to the Ed-Fi Alliance under one or more agreements.\nThe Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.\nSee the LICENSE and NOTICES files in the project root for more information.
indent_style=space
indent_size=4
tab_width=2
@@ -34,19 +35,120 @@ resharper_redundant_base_qualifier_highlighting=warning
resharper_suggest_var_or_type_built_in_types_highlighting=hint
resharper_suggest_var_or_type_elsewhere_highlighting=hint
resharper_suggest_var_or_type_simple_types_highlighting=hint
-# Microsoft .NET properties
-csharp_new_line_before_members_in_object_initializers=false
-csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
-csharp_style_var_elsewhere=true:suggestion
-csharp_style_var_for_built_in_types=true:suggestion
-csharp_style_var_when_type_is_apparent=true:suggestion
-dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:none
-dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:none
-dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:none
-dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion
-dotnet_style_predefined_type_for_member_access=true:suggestion
-dotnet_style_qualification_for_event=false:suggestion
-dotnet_style_qualification_for_field=false:suggestion
-dotnet_style_qualification_for_method=false:suggestion
-dotnet_style_qualification_for_property=false:suggestion
-dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion
+# Standard Ed-Fi C# settings below
+indent_style = space
+indent_size = 4
+tab_width = 4
+csharp_new_line_before_members_in_object_initializers = false
+csharp_preferred_modifier_order = private, public, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
+
+dotnet_diagnostic.IDE0073.severity=error # File header template required
+
+# Formatting rules
+dotnet_diagnostic.IDE0055.severity=warning
+csharp_new_line_before_open_brace = all # Allman style
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = no_change
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents_when_block = true
+csharp_space_after_cast = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_between_parentheses = none
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_after_comma = true
+csharp_space_before_comma = false
+csharp_space_after_dot = false
+csharp_space_before_dot = false
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_around_declaration_statements = false
+csharp_space_before_open_square_brackets = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_square_brackets = false
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = false
+
+# Naming rules
+dotnet_naming_rule.local_constants_rule.severity = warning
+dotnet_naming_rule.local_constants_rule.style = upper_camel_case_style
+dotnet_naming_rule.local_constants_rule.symbols = local_constants_symbols
+dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.private_constants_rule.severity = warning
+dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style
+dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
+dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.private_static_readonly_rule.severity = warning
+dotnet_naming_rule.private_static_readonly_rule.style = lower_camel_case_style
+dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols
+dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
+dotnet_naming_style.lower_camel_case_style.required_prefix = _
+dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case
+dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.local_constants_symbols.applicable_kinds = local
+dotnet_naming_symbols.local_constants_symbols.required_modifiers = const
+dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field
+dotnet_naming_symbols.private_constants_symbols.required_modifiers = const
+dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
+dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly
+
+# Misc style
+dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
+dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none
+dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_sort_system_directives_first = true
+dotnet_separate_import_directive_groups = false
+
+# We *like* var
+dotnet_diagnostic.IDE0008.severity=none # IDE0008: Use explicit type
+csharp_style_var_elsewhere = false:none
+csharp_style_var_for_built_in_types = false:suggestion
+
+# Using statements
+csharp_using_directive_placement=outside_namespace:warning
+dotnet_diagnostic.IDE0065.severity=warning # placement of using statements
+dotnet_diagnostic.IDE0005.severity=suggestion # Remove unnecessary using directives
+
+# Lower the priority
+dotnet_diagnostic.S4136.severity=suggestion # Method overloads should be grouped together
+dotnet_diagnostic.S1135.severity=suggestion # Complete TODO comments
+dotnet_diagnostic.S112.severity=suggestion # 'System.Exception' should not be thrown by user code.
+
+# Allow empty records for discriminated union types
+dotnet_diagnostic.S2094.severity=none # S2094: Classes should not be empty
+
+[**/tests/**/*.cs]
+# Allow our strange test class naming convention
+dotnet_naming_symbols.amt_tests.applicable_kinds = class,method
+dotnet_naming_symbols.amt_tests.word_separator = "_"
+dotnet_naming_symbols.amt_tests.capitalization = first_word_upper
+dotnet_diagnostic.IDE1006.severity=none
+dotnet_diagnostic.S101.severity=none
+
+# SonarLint doesn't understand implied assertions from FakeItEasy
+dotnet_diagnostic.S2699.severity=none # S2699: Tests should include assertions
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 000000000..aa8b9a08d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: Apache-2.0
+# Licensed to the Ed-Fi Alliance under one or more agreements.
+# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+# See the LICENSE and NOTICES files in the project root for more information.
+
+blank_issues_enabled: false
+contact_links:
+ - name: 🏠 Ed-Fi Community Hub
+ url: https://community.ed-fi.org
+ about: For technical support, questions, and community discussions
+ - name: 📚 Ed-Fi Documentation
+ url: https://docs.ed-fi.org
+ about: Official Ed-Fi technical documentation and guides
diff --git a/.github/ISSUE_TEMPLATE/engineering-issue.yml b/.github/ISSUE_TEMPLATE/engineering-issue.yml
new file mode 100644
index 000000000..c008e2548
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/engineering-issue.yml
@@ -0,0 +1,47 @@
+# SPDX-License-Identifier: Apache-2.0
+# Licensed to the Ed-Fi Alliance under one or more agreements.
+# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+# See the LICENSE and NOTICES files in the project root for more information.
+
+name: "🔧 Engineering Team Issue"
+description: "Internal issue for Ed-Fi engineering team members only"
+title: "Feature: [Brief Description of the Issue]"
+labels: []
+assignees: []
+body:
+ - type: markdown
+ attributes:
+ value: |
+ ## ⚠️ Engineering Team Only
+
+ This template is for Ed-Fi engineering team members only.
+
+ If you're not part of the engineering team, please use the [Ed-Fi Community Hub](https://community.ed-fi.org) for support.
+
+ - type: textarea
+ id: description
+ attributes:
+ label: "Issue Description"
+ description: "Describe the issue or task"
+ placeholder: "Clear description of the issue..."
+ validations:
+ required: true
+
+ - type: textarea
+ id: acceptance-criteria
+ attributes:
+ label: "Acceptance Criteria"
+ description: "What needs to be done to resolve this issue?"
+ placeholder: "- [ ] Criteria 1\n- [ ] Criteria 2"
+ validations:
+ required: false
+
+ - type: textarea
+ id: additional-context
+ attributes:
+ label: "Additional Context"
+ description: "Any additional information, screenshots, or context"
+ placeholder: "Additional details..."
+ validations:
+ required: false
+
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 000000000..bb067b12b
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,30 @@
+## General
+
+* Make only high confidence suggestions when reviewing code changes.
+* Never change NuGet.config files unless explicitly asked to.
+
+## Formatting
+
+* Apply code-formatting style defined in `.editorconfig`.
+* Prefer file-scoped namespace declarations and single-line using directives.
+* Insert a newline before the opening curly brace of any code block (e.g., after `if`, `for`, `while`, `foreach`, `using`, `try`, etc.).
+* Ensure that the final return statement of a method is on its own line.
+* Use pattern matching and switch expressions wherever possible.
+* Use `nameof` instead of string literals when referring to member names.
+
+### Nullable Reference Types
+
+* Declare variables non-nullable, and check for `null` at entry points.
+* Always use `is null` or `is not null` instead of `== null` or `!= null`.
+* Trust the C# null annotations and don't add null checks when the type system says a value cannot be null.
+
+### Testing
+
+* We use NUnit tests.
+* We use Shouldly for assertions.
+* Use FakeItEasy for mocking in tests.
+* Copy existing style in nearby files for test method names and capitalization.
+
+## Running tests
+
+* To build and run tests in the repo, use the command `./build.ps1 UnitTest`
diff --git a/.github/workflows/after-pullrequest.yml b/.github/workflows/after-pullrequest.yml
index a95779421..91bb6afe0 100644
--- a/.github/workflows/after-pullrequest.yml
+++ b/.github/workflows/after-pullrequest.yml
@@ -22,7 +22,6 @@ jobs:
permissions:
checks: write
pull-requests: write
- # actions: read
# only needed for private repository
# contents: read
# issues: read
diff --git a/.github/workflows/api-e2e-mssql-multitenant.yml b/.github/workflows/api-e2e-mssql-multitenant.yml
new file mode 100644
index 000000000..791fb466c
--- /dev/null
+++ b/.github/workflows/api-e2e-mssql-multitenant.yml
@@ -0,0 +1,147 @@
+# SPDX-License-Identifier: Apache-2.0
+# Licensed to the Ed-Fi Alliance under one or more agreements.
+# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+# See the LICENSE and NOTICES files in the project root for more information.
+
+name: Admin API Multi Tenant E2E Tests + ODS 7 + Mssql
+
+on:
+ push:
+ branches: [main]
+ schedule:
+ - cron: "0 5 * * 1"
+ workflow_dispatch:
+ pull_request:
+ branches: [main]
+
+env:
+ JIRA_ACCESS_TOKEN: ${{ secrets.JIRA_ACCESS_TOKEN }}
+ ADMIN_API_VERSION: "2.2.0"
+ PROJECT_ID: "13401"
+ CYCLE_NAME: "Automation Cycle"
+ TASK_NAME: "API Automation Task"
+ FOLDER_NAME: "API Automation Run"
+ RESULTS_FILE: "test-xml-results"
+
+permissions: read-all
+
+jobs:
+ run-e2e-multitenant-tests:
+ defaults:
+ run:
+ working-directory: ./Application/EdFi.Ods.AdminApi/
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Copy application folder to docker context
+ run: |
+ mkdir ../../Docker/Application
+ cp -r ../EdFi.Ods.AdminApi ../../Docker/Application
+
+ - name: Copy admin api common folder to docker context
+ run: cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application
+
+ - name: Copy adminApi V1 folder to docker context
+ run: cp -r ../EdFi.Ods.AdminApi.V1 ../../Docker/Application
+
+ - name: Copy nuget config to docker context
+ run: cp ../NuGet.Config ../../Docker/Application
+
+ - name: Update certificates
+ run: cp -r ../../eng/test-certs/ssl ../../Docker/Settings
+
+ - name: Run Admin API
+ run: |
+ docker compose \
+ -f '../../Docker/V2/Compose/mssql/MultiTenant/compose-build-dev-multi-tenant.yml' \
+ --env-file './E2E Tests/V2/gh-action-setup/.automation_mssql.env' \
+ up -d
+
+ - name: Verify containers are running
+ run: |
+ chmod +x './E2E Tests/V2/gh-action-setup/admin_inspect.sh'
+ './E2E Tests/V2/gh-action-setup/admin_inspect.sh' adminapi
+
+ - name: Setup node
+ uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
+ with:
+ node-version: "20"
+
+ - name: Combine Collections
+ run: |
+ npx postman-combine-collections@1.1.2 -f './E2E Tests/V2/Admin API E2E 2.0 -*.postman_collection.json' \
+ -n 'Admin API E2E 2.0' \
+ -o './E2E Tests/V2/Admin-API-Full-Collection.json'
+
+ - name: Run tests
+ run: |
+ npx --package=newman@6.1.3 --package=newman-reporter-htmlextra@1.23.1 newman run './E2E Tests/V2/Admin-API-Full-Collection.json' \
+ -e './E2E Tests/V2/Admin API Docker-multitenant-mssql.postman_environment.json' \
+ -r cli,junit,htmlextra \
+ --reporter-htmlextra-title 'AdminAPI - 2.0' \
+ --reporter-htmlextra-export './report-html/results.html' \
+ --reporter-junit-export ./reports/report.xml \
+ -k
+
+ - name: Get Docker logs
+ if: failure()
+ run: |
+ mkdir docker-logs
+ docker logs ed-fi-db-admin-adminapi-tenant1 > docker-logs/ed-fi-db-admin-adminapi-tenant1.log
+ docker logs ed-fi-db-admin-adminapi-tenant2 > docker-logs/ed-fi-db-admin-adminapi-tenant2.log
+ docker logs adminapi > docker-logs/adminapi.log
+
+ - name: Upload Docker logs
+ if: failure()
+ uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
+ with:
+ name: docker-logs
+ path: Application/EdFi.Ods.AdminApi/docker-logs/
+ retention-days: 5
+
+ - name: Upload Html results
+ uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
+ if: success() || failure()
+ with:
+ name: test-html-results
+ path: Application/EdFi.Ods.AdminApi/report-html/results.html
+ retention-days: 5
+
+ - name: Upload xml results
+ uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
+ if: success() || failure()
+ with:
+ name: test-xml-results
+ path: Application/EdFi.Ods.AdminApi/reports/report.xml
+ retention-days: 5
+
+ report:
+ defaults:
+ run:
+ shell: pwsh
+ needs:
+ - run-e2e-multitenant-tests
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Download artifacts
+ uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8
+
+ # Commented code until Token is renewed
+ # - name: Send report to Zephyr
+ # run: |
+ # $parameters = @{
+ # cycleName = '${{ env.CYCLE_NAME }}'
+ # taskName = '${{ env.TASK_NAME }}'
+ # folderName = '${{ env.FOLDER_NAME }}'
+ # }
+ # .\eng\send-test-results.ps1 `
+ # -PersonalAccessToken ${{ env.JIRA_ACCESS_TOKEN }} `
+ # -ProjectId ${{ env.PROJECT_ID }} `
+ # -AdminApiVersion '${{ env.ADMIN_API_VERSION }}' `
+ # -ResultsFilePath '${{ env.RESULTS_FILE }}/report.xml' `
+ # -ConfigParams $parameters
diff --git a/.github/workflows/api-e2e-mssql-singletenant.yml b/.github/workflows/api-e2e-mssql-singletenant.yml
new file mode 100644
index 000000000..6398af6dd
--- /dev/null
+++ b/.github/workflows/api-e2e-mssql-singletenant.yml
@@ -0,0 +1,146 @@
+# SPDX-License-Identifier: Apache-2.0
+# Licensed to the Ed-Fi Alliance under one or more agreements.
+# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+# See the LICENSE and NOTICES files in the project root for more information.
+
+name: Admin API Single Tenant E2E Tests + ODS 7 + Mssql
+
+on:
+ push:
+ branches: [main]
+ schedule:
+ - cron: "0 5 * * 1"
+ workflow_dispatch:
+ pull_request:
+ branches: [main]
+
+env:
+ JIRA_ACCESS_TOKEN: ${{ secrets.JIRA_ACCESS_TOKEN }}
+ ADMIN_API_VERSION: "2.2.0"
+ PROJECT_ID: "13401"
+ CYCLE_NAME: "Automation Cycle"
+ TASK_NAME: "API Automation Task"
+ FOLDER_NAME: "API Automation Run"
+ RESULTS_FILE: "test-xml-results"
+
+permissions: read-all
+
+jobs:
+ run-e2e-tests:
+ defaults:
+ run:
+ working-directory: ./Application/EdFi.Ods.AdminApi/
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Copy application folder to docker context
+ run: |
+ mkdir ../../Docker/Application
+ cp -r ../EdFi.Ods.AdminApi ../../Docker/Application
+
+ - name: Copy admin api common folder to docker context
+ run: cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application
+
+ - name: Copy adminApi V1 folder to docker context
+ run: cp -r ../EdFi.Ods.AdminApi.V1 ../../Docker/Application
+
+ - name: Copy nuget config to docker context
+ run: cp ../NuGet.Config ../../Docker/Application
+
+ - name: Update certificates
+ run: cp -r ../../eng/test-certs/ssl ../../Docker/Settings
+
+ - name: Run Admin API
+ run: |
+ docker compose \
+ -f '../../Docker/V2/Compose/mssql/SingleTenant/compose-build-dev.yml' \
+ --env-file './E2E Tests/V2/gh-action-setup/.automation_mssql.env' \
+ up -d
+
+ - name: Verify containers are running
+ run: |
+ chmod +x './E2E Tests/V2/gh-action-setup/admin_inspect.sh'
+ './E2E Tests/V2/gh-action-setup/admin_inspect.sh' adminapi
+
+ - name: Setup node
+ uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
+ with:
+ node-version: "16"
+
+ - name: Combine Collections
+ run: |
+ npx postman-combine-collections@1.1.2 -f './E2E Tests/V2/Admin API E2E 2.0 -*.postman_collection.json' \
+ -n 'Admin API E2E 2.0' \
+ -o './E2E Tests/V2/Admin-API-Full-Collection.json'
+
+ - name: Run tests
+ run: |
+ npx --package=newman@6.1.3 --package=newman-reporter-htmlextra@1.23.1 newman run './E2E Tests/V2/Admin-API-Full-Collection.json' \
+ -e './E2E Tests/V2/Admin API Docker-single-mssql.postman_environment.json' \
+ -r cli,junit,htmlextra \
+ --reporter-htmlextra-title 'AdminAPI - 2.0' \
+ --reporter-htmlextra-export './report-html/results.html' \
+ --reporter-junit-export ./reports/report.xml \
+ -k
+
+ - name: Get Docker logs
+ if: failure()
+ run: |
+ mkdir docker-logs
+ docker logs ed-fi-db-admin-adminapi > docker-logs/ed-fi-db-admin-adminapi.log
+ docker logs adminapi > docker-logs/adminapi.log
+
+ - name: Upload Docker logs
+ if: failure()
+ uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
+ with:
+ name: docker-logs
+ path: Application/EdFi.Ods.AdminApi/docker-logs/
+ retention-days: 5
+
+ - name: Upload Html results
+ uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
+ if: success() || failure()
+ with:
+ name: test-html-results
+ path: Application/EdFi.Ods.AdminApi/report-html/results.html
+ retention-days: 5
+
+ - name: Upload xml results
+ uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
+ if: success() || failure()
+ with:
+ name: test-xml-results
+ path: Application/EdFi.Ods.AdminApi/reports/report.xml
+ retention-days: 5
+
+ report:
+ defaults:
+ run:
+ shell: pwsh
+ needs:
+ - run-e2e-tests
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Download artifacts
+ uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8
+
+ # Commented code until Token is renewed
+ # - name: Send report to Zephyr
+ # run: |
+ # $parameters = @{
+ # cycleName = '${{ env.CYCLE_NAME }}'
+ # taskName = '${{ env.TASK_NAME }}'
+ # folderName = '${{ env.FOLDER_NAME }}'
+ # }
+ # .\eng\send-test-results.ps1 `
+ # -PersonalAccessToken ${{ env.JIRA_ACCESS_TOKEN }} `
+ # -ProjectId ${{ env.PROJECT_ID }} `
+ # -AdminApiVersion '${{ env.ADMIN_API_VERSION }}' `
+ # -ResultsFilePath '${{ env.RESULTS_FILE }}/report.xml' `
+ # -ConfigParams $parameters
diff --git a/.github/workflows/api-e2e-pgsql-multitenant.yml b/.github/workflows/api-e2e-pgsql-multitenant.yml
new file mode 100644
index 000000000..f90d7676a
--- /dev/null
+++ b/.github/workflows/api-e2e-pgsql-multitenant.yml
@@ -0,0 +1,147 @@
+# SPDX-License-Identifier: Apache-2.0
+# Licensed to the Ed-Fi Alliance under one or more agreements.
+# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+# See the LICENSE and NOTICES files in the project root for more information.
+
+name: Admin API Multi Tenant E2E Tests + ODS 7 + Pgsql
+
+on:
+ push:
+ branches: [main]
+ schedule:
+ - cron: "0 5 * * 1"
+ workflow_dispatch:
+ pull_request:
+ branches: [main]
+
+env:
+ JIRA_ACCESS_TOKEN: ${{ secrets.JIRA_ACCESS_TOKEN }}
+ ADMIN_API_VERSION: "2.2.0"
+ PROJECT_ID: "13401"
+ CYCLE_NAME: "Automation Cycle"
+ TASK_NAME: "API Automation Task"
+ FOLDER_NAME: "API Automation Run"
+ RESULTS_FILE: "test-xml-results"
+
+permissions: read-all
+
+jobs:
+ run-e2e-multitenant-tests:
+ defaults:
+ run:
+ working-directory: ./Application/EdFi.Ods.AdminApi/
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Copy application folder to docker context
+ run: |
+ mkdir ../../Docker/Application
+ cp -r ../EdFi.Ods.AdminApi ../../Docker/Application
+
+ - name: Copy admin api common folder to docker context
+ run: cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application
+
+ - name: Copy adminApi V1 folder to docker context
+ run: cp -r ../EdFi.Ods.AdminApi.V1 ../../Docker/Application
+
+ - name: Copy nuget config to docker context
+ run: cp ../NuGet.Config ../../Docker/Application
+
+ - name: Update certificates
+ run: cp -r ../../eng/test-certs/ssl ../../Docker/Settings
+
+ - name: Run Admin API
+ run: |
+ docker compose \
+ -f '../../Docker/V2/Compose/pgsql/MultiTenant/compose-build-dev-multi-tenant.yml' \
+ --env-file './E2E Tests/V2/gh-action-setup/.automation_pgsql.env' \
+ up -d
+
+ - name: Verify containers are running
+ run: |
+ chmod +x './E2E Tests/V2/gh-action-setup/admin_inspect.sh'
+ './E2E Tests/V2/gh-action-setup/admin_inspect.sh' adminapi
+
+ - name: Setup node
+ uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
+ with:
+ node-version: "20"
+
+ - name: Combine Collections
+ run: |
+ npx postman-combine-collections@1.1.2 -f './E2E Tests/V2/Admin API E2E 2.0 -*.postman_collection.json' \
+ -n 'Admin API E2E 2.0' \
+ -o './E2E Tests/V2/Admin-API-Full-Collection.json'
+
+ - name: Run tests
+ run: |
+ npx --package=newman@6.1.3 --package=newman-reporter-htmlextra@1.23.1 newman run './E2E Tests/V2/Admin-API-Full-Collection.json' \
+ -e './E2E Tests/V2/Admin API Docker-multitenant-pgsql.postman_environment.json' \
+ -r cli,junit,htmlextra \
+ --reporter-htmlextra-title 'AdminAPI - 2.0' \
+ --reporter-htmlextra-export './report-html/results.html' \
+ --reporter-junit-export ./reports/report.xml \
+ -k
+
+ - name: Get Docker logs
+ if: failure()
+ run: |
+ mkdir docker-logs
+ docker logs ed-fi-db-admin-adminapi-tenant1 > docker-logs/ed-fi-db-admin-adminapi-tenant1.log
+ docker logs ed-fi-db-admin-adminapi-tenant2 > docker-logs/ed-fi-db-admin-adminapi-tenant2.log
+ docker logs adminapi > docker-logs/adminapi.log
+
+ - name: Upload Docker logs
+ if: failure()
+ uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
+ with:
+ name: docker-logs
+ path: Application/EdFi.Ods.AdminApi/docker-logs/
+ retention-days: 5
+
+ - name: Upload Html results
+ uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
+ if: success() || failure()
+ with:
+ name: test-html-results
+ path: Application/EdFi.Ods.AdminApi/report-html/results.html
+ retention-days: 5
+
+ - name: Upload xml results
+ uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
+ if: success() || failure()
+ with:
+ name: test-xml-results
+ path: Application/EdFi.Ods.AdminApi/reports/report.xml
+ retention-days: 5
+
+ report:
+ defaults:
+ run:
+ shell: pwsh
+ needs:
+ - run-e2e-multitenant-tests
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Download artifacts
+ uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8
+
+ # Commented code until Token is renewed
+ # - name: Send report to Zephyr
+ # run: |
+ # $parameters = @{
+ # cycleName = '${{ env.CYCLE_NAME }}'
+ # taskName = '${{ env.TASK_NAME }}'
+ # folderName = '${{ env.FOLDER_NAME }}'
+ # }
+ # .\eng\send-test-results.ps1 `
+ # -PersonalAccessToken ${{ env.JIRA_ACCESS_TOKEN }} `
+ # -ProjectId ${{ env.PROJECT_ID }} `
+ # -AdminApiVersion '${{ env.ADMIN_API_VERSION }}' `
+ # -ResultsFilePath '${{ env.RESULTS_FILE }}/report.xml' `
+ # -ConfigParams $parameters
diff --git a/.github/workflows/api-e2e-pgsql-singletenant.yml b/.github/workflows/api-e2e-pgsql-singletenant.yml
new file mode 100644
index 000000000..3632dc752
--- /dev/null
+++ b/.github/workflows/api-e2e-pgsql-singletenant.yml
@@ -0,0 +1,142 @@
+# SPDX-License-Identifier: Apache-2.0
+# Licensed to the Ed-Fi Alliance under one or more agreements.
+# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+# See the LICENSE and NOTICES files in the project root for more information.
+
+name: Admin API Single Tenant E2E Tests + ODS 7 + Pgsql
+
+on:
+ push:
+ branches: [main]
+ schedule:
+ - cron: "0 5 * * 1"
+ workflow_dispatch:
+ pull_request:
+ branches: [main]
+
+env:
+ JIRA_ACCESS_TOKEN: ${{ secrets.JIRA_ACCESS_TOKEN }}
+ ADMIN_API_VERSION: "2.2.0"
+ PROJECT_ID: "13401"
+ CYCLE_NAME: "Automation Cycle"
+ TASK_NAME: "API Automation Task"
+ FOLDER_NAME: "API Automation Run"
+ RESULTS_FILE: "test-xml-results"
+
+permissions: read-all
+
+jobs:
+ run-e2e-tests:
+ defaults:
+ run:
+ working-directory: ./Application/EdFi.Ods.AdminApi/
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Copy application folder to docker context
+ run: |
+ mkdir ../../Docker/Application
+ cp -r ../EdFi.Ods.AdminApi ../../Docker/Application
+ cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application
+ cp -r ../EdFi.Ods.AdminApi.V1 ../../Docker/Application
+
+ - name: Copy nuget config to docker context
+ run: cp ../NuGet.Config ../../Docker/Application
+
+ - name: Update certificates
+ run: cp -r ../../eng/test-certs/ssl ../../Docker/Settings
+
+ - name: Run Admin API
+ run: |
+ docker compose \
+ -f '../../Docker/V2/Compose/pgsql/SingleTenant/compose-build-dev.yml' \
+ --env-file './E2E Tests/V2/gh-action-setup/.automation_pgsql.env' \
+ up -d
+
+ - name: Verify containers are running
+ run: |
+ chmod +x './E2E Tests/V2/gh-action-setup/admin_inspect.sh'
+ './E2E Tests/V2/gh-action-setup/admin_inspect.sh' adminapi
+
+ - name: Setup node
+ uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
+ with:
+ node-version: "16"
+
+ - name: Combine Collections
+ run: |
+ npx postman-combine-collections@1.1.2 -f './E2E Tests/V2/Admin API E2E 2.0 -*.postman_collection.json' \
+ -n 'Admin API E2E 2.0' \
+ -o './E2E Tests/V2/Admin-API-Full-Collection.json'
+
+ - name: Run tests
+ run: |
+ npx --package=newman@6.1.3 --package=newman-reporter-htmlextra@1.23.1 newman run './E2E Tests/V2/Admin-API-Full-Collection.json' \
+ -e './E2E Tests/V2/Admin API Docker-single-pgsql.postman_environment.json' \
+ -r cli,junit,htmlextra \
+ --reporter-htmlextra-title 'AdminAPI - 2.0' \
+ --reporter-htmlextra-export './report-html/results.html' \
+ --reporter-junit-export ./reports/report.xml \
+ -k
+
+ - name: Get Docker logs
+ if: failure()
+ run: |
+ mkdir docker-logs
+ docker logs ed-fi-db-admin-adminapi > docker-logs/ed-fi-db-admin-adminapi.log
+ docker logs adminapi > docker-logs/adminapi.log
+
+ - name: Upload Docker logs
+ if: failure()
+ uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
+ with:
+ name: docker-logs
+ path: Application/EdFi.Ods.AdminApi/docker-logs/
+ retention-days: 5
+
+ - name: Upload Html results
+ uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
+ if: success() || failure()
+ with:
+ name: test-html-results
+ path: Application/EdFi.Ods.AdminApi/report-html/results.html
+ retention-days: 5
+
+ - name: Upload xml results
+ uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
+ if: success() || failure()
+ with:
+ name: test-xml-results
+ path: Application/EdFi.Ods.AdminApi/reports/report.xml
+ retention-days: 5
+
+ report:
+ defaults:
+ run:
+ shell: pwsh
+ needs:
+ - run-e2e-tests
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Download artifacts
+ uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8
+
+ # Commented code until Token is renewed
+ # - name: Send report to Zephyr
+ # run: |
+ # $parameters = @{
+ # cycleName = '${{ env.CYCLE_NAME }}'
+ # taskName = '${{ env.TASK_NAME }}'
+ # folderName = '${{ env.FOLDER_NAME }}'
+ # }
+ # .\eng\send-test-results.ps1 `
+ # -PersonalAccessToken ${{ env.JIRA_ACCESS_TOKEN }} `
+ # -ProjectId ${{ env.PROJECT_ID }} `
+ # -AdminApiVersion '${{ env.ADMIN_API_VERSION }}' `
+ # -ResultsFilePath '${{ env.RESULTS_FILE }}/report.xml' `
+ # -ConfigParams $parameters
diff --git a/.github/workflows/api-e2e-mssql.yml b/.github/workflows/api-v1-e2e-mssql.yml
similarity index 89%
rename from .github/workflows/api-e2e-mssql.yml
rename to .github/workflows/api-v1-e2e-mssql.yml
index e92c1324e..71e6d816c 100644
--- a/.github/workflows/api-e2e-mssql.yml
+++ b/.github/workflows/api-v1-e2e-mssql.yml
@@ -50,14 +50,14 @@ jobs:
- name: Run Admin API
run: |
docker compose \
- -f '../../Docker/Compose/mssql/compose-build-dev.yml' \
- --env-file './E2E Tests/gh-action-setup/.automation.env' \
+ -f '../../Docker/V1/Compose/mssql/compose-build-dev.yml' \
+ --env-file './E2E Tests/V1/gh-action-setup/.automation.env' \
up -d
- name: Verify containers are running
run: |
- chmod +x './E2E Tests/gh-action-setup/inspect.sh'
- './E2E Tests/gh-action-setup/inspect.sh'
+ chmod +x './E2E Tests/V1/gh-action-setup/inspect.sh'
+ './E2E Tests/V1/gh-action-setup/inspect.sh'
- name: Setup node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@@ -66,14 +66,14 @@ jobs:
- name: Combine Collections
run: |
- npx postman-combine-collections@1.1.2 -f './E2E Tests/Admin API E2E.postman_collection*.json' \
+ npx postman-combine-collections@1.1.2 -f './E2E Tests/V1/Admin API E2E.postman_collection*.json' \
-n 'Admin API E2E 1.0' \
- -o './E2E Tests/Admin-API-Full-Collection.json'
+ -o './E2E Tests/V1/Admin-API-Full-Collection.json'
- name: Run tests
run: |
- npx --package=newman@6.1.2 --package=newman-reporter-htmlextra@1.23.1 -- newman run './E2E Tests/Admin-API-Full-Collection.json' \
- -e './E2E Tests/Admin API Docker.postman_environment.json' \
+ npx --package=newman@6.1.2 --package=newman-reporter-htmlextra@1.23.1 -- newman run './E2E Tests/V1/Admin-API-Full-Collection.json' \
+ -e './E2E Tests/V1/Admin API Docker.postman_environment.json' \
-r cli,junit,htmlextra \
--reporter-htmlextra-title 'AdminAPI - 1.0' \
--reporter-htmlextra-export './report-html/results.html' \
diff --git a/.github/workflows/api-e2e-pgsql.yml b/.github/workflows/api-v1-e2e-pgsql.yml
similarity index 89%
rename from .github/workflows/api-e2e-pgsql.yml
rename to .github/workflows/api-v1-e2e-pgsql.yml
index c4206d4e3..ccb8ddf96 100644
--- a/.github/workflows/api-e2e-pgsql.yml
+++ b/.github/workflows/api-v1-e2e-pgsql.yml
@@ -50,14 +50,14 @@ jobs:
- name: Run Admin API
run: |
docker compose \
- -f '../../Docker/Compose/pgsql/compose-build-dev.yml' \
- --env-file './E2E Tests/gh-action-setup/.automation.env' \
+ -f '../../Docker/V1/Compose/pgsql/compose-build-dev.yml' \
+ --env-file './E2E Tests/V1/gh-action-setup/.automation.env' \
up -d
- name: Verify containers are running
run: |
- chmod +x './E2E Tests/gh-action-setup/inspect.sh'
- './E2E Tests/gh-action-setup/inspect.sh'
+ chmod +x './E2E Tests/V1/gh-action-setup/inspect.sh'
+ './E2E Tests/V1/gh-action-setup/inspect.sh'
- name: Setup node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@@ -66,14 +66,14 @@ jobs:
- name: Combine Collections
run: |
- npx postman-combine-collections@1.1.2 -f './E2E Tests/Admin API E2E.postman_collection*.json' \
+ npx postman-combine-collections@1.1.2 -f './E2E Tests/V1/Admin API E2E.postman_collection*.json' \
-n 'Admin API E2E 1.0' \
- -o './E2E Tests/Admin-API-Full-Collection.json'
+ -o './E2E Tests/V1/Admin-API-Full-Collection.json'
- name: Run tests
run: |
- npx --package=newman@6.1.2 --package=newman-reporter-htmlextra@1.23.1 -- newman run './E2E Tests/Admin-API-Full-Collection.json' \
- -e './E2E Tests/Admin API Docker.postman_environment.json' \
+ npx --package=newman@6.1.2 --package=newman-reporter-htmlextra@1.23.1 -- newman run './E2E Tests/V1/Admin-API-Full-Collection.json' \
+ -e './E2E Tests/V1/Admin API Docker.postman_environment.json' \
-r cli,junit,htmlextra \
--reporter-htmlextra-title 'AdminAPI - 1.0' \
--reporter-htmlextra-export './report-html/results.html' \
diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml
new file mode 100644
index 000000000..be533e2b5
--- /dev/null
+++ b/.github/workflows/copilot-setup-steps.yml
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: Apache-2.0
+# Licensed to the Ed-Fi Alliance under one or more agreements.
+# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+# See the LICENSE and NOTICES files in the project root for more information.
+
+name: Copilot Setup Steps
+
+on:
+ workflow_dispatch:
+ push:
+ paths:
+ - ".github/workflows/copilot-setup-steps.yml"
+ pull_request:
+ paths:
+ - ".github/workflows/copilot-setup-steps.yml"
+
+permissions:
+ contents: read
+
+jobs:
+ copilot-setup-steps:
+ name: Copilot Setup Steps
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Install NuGet package dependencies
+ run: dotnet restore Application/Ed-Fi-ODS-AdminApi.sln
\ No newline at end of file
diff --git a/.github/workflows/on-issue-opened.yml b/.github/workflows/on-issue-opened.yml
new file mode 100644
index 000000000..938b97baf
--- /dev/null
+++ b/.github/workflows/on-issue-opened.yml
@@ -0,0 +1,72 @@
+# SPDX-License-Identifier: Apache-2.0
+# Licensed to the Ed-Fi Alliance under one or more agreements.
+# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+# See the LICENSE and NOTICES files in the project root for more information.
+
+name: "Issue Management"
+
+on:
+ issues:
+ types: [opened]
+
+permissions:
+ issues: write
+
+jobs:
+ manage-community-issues:
+ runs-on: ubuntu-latest
+ if: github.event.issue.user.type != 'bot'
+ steps:
+ - name: Check if user has repository access
+ id: check-repo-access
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ with:
+ script: |
+ try {
+ // Check if user has repository access (collaborator, maintainer, admin, etc.)
+ const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ username: context.payload.issue.user.login
+ });
+
+ console.log(`User ${context.payload.issue.user.login} repository permission: ${permission.permission}`);
+ return ['admin', 'maintain', 'write', 'triage'].includes(permission.permission);
+ } catch (error) {
+ console.log(`User ${context.payload.issue.user.login} does not have repository access: ${error.message}`);
+ return false;
+ }
+
+ - name: Close community issue with helpful message
+ if: steps.check-repo-access.outputs.result == 'false'
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ with:
+ script: |
+ const message = `Thank you for your interest in Ed-Fi!
+
+ This GitHub repository is maintained by the Ed-Fi engineering team for internal issue tracking and development coordination.
+
+ For technical support, questions, and community discussions, please visit the **[Ed-Fi Community Hub](https://community.ed-fi.org)** where our community and support team can better assist you.
+
+ The Ed-Fi Community Hub provides:
+ - Technical support and troubleshooting
+ - Community discussions and best practices
+ - Documentation and resources
+ - Direct access to Ed-Fi experts
+
+ We appreciate your understanding and look forward to helping you on the Community Hub!`;
+
+ await github.rest.issues.createComment({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: message
+ });
+
+ await github.rest.issues.update({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'closed',
+ state_reason: 'not_planned'
+ });
diff --git a/.github/workflows/on-prerelease.yml b/.github/workflows/on-prerelease.yml
index 456a47745..7c7e4ebbf 100644
--- a/.github/workflows/on-prerelease.yml
+++ b/.github/workflows/on-prerelease.yml
@@ -11,8 +11,8 @@ on:
env:
ARTIFACTS_API_KEY: ${{ secrets.AZURE_ARTIFACTS_PERSONAL_ACCESS_TOKEN }}
- ARTIFACTS_FEED_URL: "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_packaging/EdFi/nuget/v3/index.json"
- VSS_NUGET_EXTERNAL_FEED_ENDPOINTS: '{"endpointCredentials": [{"endpoint": "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_packaging/EdFi/nuget/v3/index.json","password": "${{ secrets.AZURE_ARTIFACTS_PERSONAL_ACCESS_TOKEN }}"}]}'
+ ARTIFACTS_FEED_URL: ${{ vars.AZURE_ARTIFACTS_FEED_URL }}
+ VSS_NUGET_EXTERNAL_FEED_ENDPOINTS: '{"endpointCredentials": [{"endpoint": "${{ vars.AZURE_ARTIFACTS_FEED_URL }}","password": "${{ secrets.AZURE_ARTIFACTS_PERSONAL_ACCESS_TOKEN }}"}]}'
MANIFEST_FILE: "_manifest/spdx_2.2/manifest.spdx.json"
PACKAGE_NAME: "AdminApi"
IMAGE_NAME: ${{ vars.IMAGE_NAME }}
@@ -63,26 +63,75 @@ jobs:
-Configuration Release `
-APIVersion $apiVersion
+ - name: Get Assembly File Version
+ id: assembly-version
+ shell: pwsh
+ run: |
+ $dllPath = "./Application/EdFi.Ods.AdminApi/publish/EdFi.Ods.AdminApi.dll"
+
+ if (-not (Test-Path $dllPath))
+ {
+ throw "Could not find EdFi.Ods.AdminApi.dll at $dllPath"
+ }
+
+ $fileVersion = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($dllPath).FileVersion
+ "file-version=$fileVersion" >> $env:GITHUB_OUTPUT
+
- name: Setup Nuget.exe
uses: nuget/setup-nuget@323ab0502cd38fdc493335025a96c8fdb0edc71f #v2.0.1
- name: Replace Installer Version Token
shell: pwsh
run: |
- $version = "${{ github.ref_name }}"
- $versionWithoutPrefix = $version -replace '^v', ''
+ $version = "${{ steps.assembly-version.outputs.file-version }}"
+
+ Write-Host "Replacing version token with: $version"
$installerPath = "Installer.AdminApi/Install-AdminApi.psm1"
- $content = Get-Content $installerPath -Raw
- $content = $content -replace '"__ADMINAPI_VERSION__"', "`"$versionWithoutPrefix`""
- $content | Set-Content $installerPath -NoNewline
+
+ if (Test-Path $installerPath)
+ {
+ $content = Get-Content $installerPath -Raw
+ $content = $content -replace '"__ADMINAPI_VERSION__"', "`"$version`""
+ $content | Set-Content $installerPath -NoNewline
+
+ Write-Host "Successfully updated version in $installerPath"
+ }
+ else
+ {
+ throw "Could not find installer module at $installerPath"
+ }
$installScriptPath = "Installer.AdminApi/install.ps1"
- $content = Get-Content $installScriptPath -Raw
- $content = $content -replace '"__ADMINAPI_VERSION__"', "`"$versionWithoutPrefix`""
- $content | Set-Content $installScriptPath -NoNewline
- Write-Output "Updated version to $version (or $versionWithoutPrefix) in all installer"
+ if (Test-Path $installScriptPath)
+ {
+ $content = Get-Content $installScriptPath -Raw
+ $content = $content -replace '"__ADMINAPI_VERSION__"', "`"$version`""
+ $content | Set-Content $installScriptPath -NoNewline
+
+ Write-Host "Successfully updated version in $installScriptPath"
+ }
+ else
+ {
+ throw "Could not find installer script at $installScriptPath"
+ }
+
+ # Verify the replacement
+ $verifyContent = Get-Content $installerPath -Raw
+
+ if ($verifyContent -match '__ADMINAPI_VERSION__')
+ {
+ throw "Token replacement failed - __ADMINAPI_VERSION__ still found in $installerPath"
+ }
+
+ $verifyInstallScriptContent = Get-Content $installScriptPath -Raw
+
+ if ($verifyInstallScriptContent -match '__ADMINAPI_VERSION__')
+ {
+ throw "Token replacement failed - __ADMINAPI_VERSION__ still found in $installScriptPath"
+ }
+ Write-Host "Verification passed - token successfully replaced with version: $version"
- name: Create NuGet Packages
if: success()
@@ -112,11 +161,10 @@ jobs:
sbom-create:
name: Create SBOM
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-latest
needs: pack
permissions:
- actions: read
- contents: write
+ actions: write
outputs:
sbom-hash-code: ${{ steps.sbom-hash-code.outputs.sbom-hash-code }}
steps:
@@ -167,7 +215,7 @@ jobs:
sbom-attach:
name: Attach SBOM file
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-latest
needs:
- sbom-create
- pack
@@ -229,7 +277,7 @@ jobs:
publish-package:
name: Publish NuGet Package
needs: pack
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-latest
defaults:
run:
shell: pwsh
@@ -257,7 +305,7 @@ jobs:
docker-publish:
name: Publish to Docker Hub
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-latest
needs:
- publish-package
steps:
@@ -278,11 +326,12 @@ jobs:
PACKAGEVERSION=${REF}
fi
- if [[ $PACKAGEVERSION =~ "alpha" ]]
+ if [[ $PACKAGEVERSION =~ "pre" ]]
then
# Pre-releases get the tag "pre"
- APITAGS="${{ env.IMAGE_NAME }}:pre-1.3"
- DBTAGS="${{ env.DATABASE_IMAGE_NAME }}:pre-1.3"
+ APITAGS="${{ env.IMAGE_NAME }}:pre"
+ DBTAGS="${{ env.DATABASE_IMAGE_NAME }}:pre"
+ DBTAGS6x="${{ env.DATABASE_IMAGE_NAME }}:pre-for-6.2"
else
# Releases get the version, plus shortened form for minor release.
# We are not using shortened form for major or using "latest"
@@ -295,17 +344,21 @@ jobs:
SEMVERSION=${PACKAGEVERSION:1} # strip off the leading 'v'
echo "APITAGS=$APITAGS" >> $GITHUB_OUTPUT
echo "DBTAGS=$DBTAGS" >> $GITHUB_OUTPUT
+ echo "DBTAGS6x=$DBTAGS6x" >> $GITHUB_OUTPUT
echo "VERSION=$SEMVERSION" >> $GITHUB_OUTPUT
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
-
- name: Log in to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: ${{ env.DOCKER_USERNAME }}
password: ${{ env.DOCKER_HUB_TOKEN }}
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
+
- name: Extract metadata (tags, labels) for admin api image
id: metaapi
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1
@@ -316,9 +369,12 @@ jobs:
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0
with:
context: "{{defaultContext}}:Docker"
- cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:pre-1.3
+ cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:pre
cache-to: type=inline
- build-args: VERSION=${{ steps.prepare-tags.outputs.VERSION }}
+ platforms: linux/amd64,linux/arm64
+ build-args: |
+ VERSION=${{ steps.prepare-tags.outputs.VERSION }}
+ ADMIN_API_VERSION=${{ steps.prepare-tags.outputs.VERSION }}
file: api.pgsql.Dockerfile
tags: ${{ steps.prepare-tags.outputs.APITAGS }}
labels: ${{ steps.metaapi.outputs.labels }}
@@ -330,14 +386,28 @@ jobs:
with:
images: ${{ env.DATABASE_IMAGE_NAME }}
- - name: Build and push admin api database image
+ - name: Build and push admin api database image 7.x
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0
with:
- context: "{{defaultContext}}:Docker/Settings/DB-Admin/pgsql"
- cache-from: type=registry,ref=${{ env.DATABASE_IMAGE_NAME }}:pre-1.3
+ context: "{{defaultContext}}:Docker/Settings/V2/DB-Admin/pgsql"
+ cache-from: type=registry,ref=${{ env.DATABASE_IMAGE_NAME }}:pre
cache-to: type=inline
- build-args: VERSION=${{ steps.prepare-tags.outputs.VERSION }}
+ platforms: linux/amd64,linux/arm64
+ build-args: ADMIN_API_VERSION=${{ steps.prepare-tags.outputs.VERSION }}
file: Dockerfile
tags: ${{ steps.prepare-tags.outputs.DBTAGS }}
labels: ${{ steps.metadatabase.outputs.labels }}
push: true
+
+ - name: Build and push admin api database image 6.x
+ uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0
+ if: startsWith(github.event.release.tag_name, 'Pre-Release') || startsWith(github.event.release.tag_name, 'v1.')
+ with:
+ context: "{{defaultContext}}:Docker/Settings/V1/DB-Admin/pgsql"
+ cache-from: type=registry,ref=${{ env.DATABASE_IMAGE_NAME }}:pre-for-6.2
+ cache-to: type=inline
+ build-args: ADMIN_API_VERSION=${{ steps.prepare-tags.outputs.VERSION }}
+ file: Dockerfile
+ tags: ${{ steps.prepare-tags.outputs.DBTAGS6x }}
+ labels: ${{ steps.metadatabase.outputs.labels }}
+ push: true
diff --git a/.github/workflows/on-pullrequest-dockerfile.yml b/.github/workflows/on-pullrequest-dockerfile.yml
index 5802bb7bf..295e1671d 100644
--- a/.github/workflows/on-pullrequest-dockerfile.yml
+++ b/.github/workflows/on-pullrequest-dockerfile.yml
@@ -34,10 +34,13 @@ jobs:
matrix:
dockerfile:
[
- { name: "api-database", path: "Docker/Settings/DB-Admin/pgsql/Dockerfile", type: "published" },
- { name: "postgres", path: "Docker/api.mssql.Dockerfile", type: "published" },
- { name: "database", path: "Docker/db.pgsql.admin.Dockerfile", type: "local" },
- { name: "database", path: "Docker/db.mssql.admin.Dockerfile", type: "local" },
+ { name: "v2-api-database", path: "Docker/Settings/V2/DB-Admin/pgsql/Dockerfile", type: "published" },
+ { name: "v1-api-database", path: "Docker/Settings/V1/DB-Admin/pgsql/Dockerfile", type: "published" },
+ { name: "postgres", path: "Docker/api.pgsql.Dockerfile", type: "published" },
+ { name: "v2-gateway", path: "Docker/Settings/V2/gateway/Dockerfile", type: "local" },
+ { name: "v1-gateway", path: "Docker/Settings/V1/gateway/Dockerfile", type: "local" },
+ { name: "v2-database", path: "Docker/V2/db.pgsql.admin.Dockerfile", type: "local" },
+ { name: "v1-database", path: "Docker/V1/db.pgsql.admin.Dockerfile", type: "local" },
{ name: "development", path: "Docker/dev.pgsql.Dockerfile", type: "local" },
]
steps:
@@ -51,8 +54,8 @@ jobs:
FEED="9f7770ac-66d9-4fbc-b81e-b5ad79002b62"
PACKAGE="db5612a0-336b-4960-9092-f3be0b63d13e"
- VERSIONS=$(curl https://feeds.dev.azure.com/ed-fi-alliance/$FEED/_apis/Packaging/Feeds/EdFi/Packages/$PACKAGE?includeAllVersions=true)
- LATEST=$(echo $VERSIONS | jq '[.versions[] | select(.views[].name == "Release") | .version | select(startswith("1."))][0]')
+ VERSIONS=$(curl https://feeds.dev.azure.com/ed-fi-alliance/$FEED/_apis/Packaging/Feeds/EdFi/Packages/$PACKAGE)
+ LATEST=$(echo $VERSIONS | jq '.versions[] | select(.isLatest == true) | .version')
echo "latest version: $LATEST"
echo "VERSION=$LATEST" >> $GITHUB_OUTPUT
@@ -75,16 +78,20 @@ jobs:
cd $folder
dockerfile=$(echo ${{matrix.dockerfile.path}} | awk -F"/" '{print $NF}')
- docker build -f $dockerfile -t ${{ matrix.dockerfile.name }} --build-context assets=.. --build-arg="VERSION=${{ steps.versions.outputs.VERSION }}" .
-
+ lower_path=$(echo "${{ matrix.dockerfile.path }}" | tr '[:upper:]' '[:lower:]')
+ if [[ "$lower_path" == *v2* || "$lower_path" == *v1* ]]
+ then
+ docker build -f $dockerfile -t ${{ matrix.dockerfile.name }} --build-context assets=../.. --build-arg="ADMIN_API_VERSION=${{ steps.versions.outputs.VERSION }}" .
+ else
+ docker build -f $dockerfile -t ${{ matrix.dockerfile.name }} --build-context assets=.. --build-arg="ADMIN_API_VERSION=${{ steps.versions.outputs.VERSION }}" .
+ fi
+
- name: Analyze
- uses: docker/scout-action@b23590dc1e4d09febc00cfcbc51e9e8c0f7ee9f3 # v1.16.1
+ uses: docker/scout-action@67eb1afe777307506aaecb9acd9a0e0389cb99ae # v1.5.0
with:
command: cves
image: local://${{ matrix.dockerfile.name }}
- sarif-file: sarif-${{ matrix.dockerfile.name }}-${{ strategy.job-index }}.output.json
- only-severities: critical,high
- only-fixed: true
+ sarif-file: sarif-${{ matrix.dockerfile.name }}.output.json
summary: true
- name: Upload SARIF result
@@ -92,4 +99,4 @@ jobs:
if: ${{ github.event_name != 'pull_request_target' }}
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 #codeql-bundle-v3.28.0
with:
- sarif_file: sarif-${{ matrix.dockerfile.name }}-${{ strategy.job-index }}.output.json
+ sarif_file: sarif-${{ matrix.dockerfile.name }}.output.json
diff --git a/.github/workflows/on-pullrequest.yml b/.github/workflows/on-pullrequest.yml
index 6b457adcf..76542f6cf 100644
--- a/.github/workflows/on-pullrequest.yml
+++ b/.github/workflows/on-pullrequest.yml
@@ -11,6 +11,11 @@ on:
- main
- "*-hotfix"
paths:
+ # TODO: restore this with AA-1601, except run in a PS-specific
+ # workflow, so that the C# build and CodeQL do not run
+ # unnecessarily.
+ # - "**/*.ps1"
+ # - "**/*.psm1"
- "**/*.cs"
- "**/*.csproj"
- ".github/**/*.yml"
@@ -19,6 +24,10 @@ on:
permissions: read-all
jobs:
+ # TODO: restore this with AA-1601
+ # run-ps-lint:
+ # name: PowerShell Linter
+ # uses: Ed-Fi-Alliance-OSS/Ed-Fi-Actions/.github/workflows/powershell-analyzer.yml@main
scan-actions-bidi:
name: Scan Actions, scan all files for BIDI Trojan Attacks
uses: ed-fi-alliance-oss/ed-fi-actions/.github/workflows/repository-scanner.yml@main
@@ -38,12 +47,25 @@ jobs:
- name: Build
run: ./build.ps1 -Command Build -Configuration Debug
+ - name: Install Coverlet Tools
+ if: success()
+ run: |
+ dotnet tool install --global coverlet.console
+ dotnet tool install --global dotnet-reportgenerator-globaltool
+
- name: Run Unit Tests
if: success()
- run: ./build.ps1 -Command UnitTest -Configuration Debug
+ run: ./build.ps1 -Command UnitTest -Configuration Debug -RunCoverageAnalysis
+
+ - name: Upload Coverage Report
+ if: always()
+ uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5
+ with:
+ name: Coverage Report
+ path: coveragereport
- name: Upload Test Results
- uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
+ uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5
with:
name: csharp-tests
path: "**/*.trx"
@@ -69,12 +91,50 @@ jobs:
if: success()
uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # codeql-bundle-v3.28.0
+ run-integration-tests:
+ name: Run integration tests
+ env:
+ DB_Password: P@55w0rd
+ runs-on: ubuntu-22.04
+ defaults:
+ run:
+ shell: pwsh
+ services:
+ sqlserver:
+ image: mcr.microsoft.com/mssql/server@sha256:d7f2c670f0cd807b4dc466b8887bd2b39a4561f624c154896f5564ea38efd13a
+ env:
+ ACCEPT_EULA: "Y"
+ SA_PASSWORD: ${{ env.DB_Password }}
+ MSSQL_ENABLE_HADR: "1"
+ ports:
+ - 1433:1433
+ steps:
+ - name: Checkout the Repo
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Setup Nuget.exe
+ uses: nuget/setup-nuget@323ab0502cd38fdc493335025a96c8fdb0edc71f #v2.0.1
+
+ - name: Build
+ run: ./build.ps1 -Command Build -Configuration Debug
+
+ - name: Run Integration Tests
+ if: success()
+ run: ./build.ps1 -Command IntegrationTest -Configuration Debug -UseIntegratedSecurity:$false -DbUsername "sa" -DbPassword ${{ env.DB_Password }}
+
+ - name: Upload Test Results
+ uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5
+ with:
+ name: csharp-integration-tests
+ path: "**/*.trx"
+ retention-days: 5
+
event_file:
name: "Event File"
runs-on: ubuntu-latest
steps:
- name: Upload
- uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
+ uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5
with:
name: Event File
path: ${{ github.event_path }}
diff --git a/.github/workflows/on-release.yml b/.github/workflows/on-release.yml
index 78fc14318..d76e2db0e 100644
--- a/.github/workflows/on-release.yml
+++ b/.github/workflows/on-release.yml
@@ -9,14 +9,15 @@ on:
types:
- released
+permissions: read-all
+
env:
ARTIFACTS_API_KEY: ${{ secrets.AZURE_ARTIFACTS_PERSONAL_ACCESS_TOKEN }}
+ ARTIFACTS_FEED_URL: "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_packaging/EdFi/nuget/v3/index.json"
ARTIFACTS_PACKAGES_URL: "https://pkgs.dev.azure.com/ed-fi-alliance/9f7770ac-66d9-4fbc-b81e-b5ad79002b62/_apis/packaging/feeds/338858dc-b976-4355-ab70-5e713b1f56be/nuget/packagesBatch?api-version=7.1-preview.1"
RELEASE_VIEW_ID: "53acfbeb-77f2-4ef6-8596-dc19e5802775" #Release
ARTIFACTS_USERNAME: ${{ secrets.AZURE_ARTIFACTS_USER_NAME }}
-permissions: read-all
-
jobs:
delete-pre-releases:
name: Delete Unnecessary Pre-Releases
@@ -60,6 +61,8 @@ jobs:
promote-Azure-artifact:
name: Promote Azure Artifact
runs-on: ubuntu-latest
+ permissions:
+ contents: read
steps:
- name: Checkout the repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
@@ -71,8 +74,8 @@ jobs:
PackagesURL = "${{ env.ARTIFACTS_PACKAGES_URL }}"
Username = "${{ env.ARTIFACTS_USERNAME }}"
ViewId = "${{ env.RELEASE_VIEW_ID }}"
- Password = (ConvertTo-SecureString -String "${{ env.ARTIFACTS_API_KEY }}" -AsPlainText -Force)
ReleaseRef = "${{ github.ref_name }}"
+ Password = (ConvertTo-SecureString -String "${{ env.ARTIFACTS_API_KEY }}" -AsPlainText -Force)
}
Import-Module ./eng/promote-packages.psm1
diff --git a/.github/workflows/openapi-md.yml b/.github/workflows/openapi-md.yml
new file mode 100644
index 000000000..7bdb15e44
--- /dev/null
+++ b/.github/workflows/openapi-md.yml
@@ -0,0 +1,78 @@
+# SPDX-License-Identifier: Apache-2.0
+# Licensed to the Ed-Fi Alliance under one or more agreements.
+# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+# See the LICENSE and NOTICES files in the project root for more information.
+
+name: Create PR to update doc and openapi definition
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version Name. Example -> 2.2.2 will result "admin-api-2.2.2.yaml" "admin-api-2.2.2-summary.md"'
+ required: true
+ type: string
+permissions: read-all
+
+env:
+ CI_COMMIT_MESSAGE: Add YAML and markdown file api-specification version ${{ inputs.version }}
+ CI_COMMIT_AUTHOR: github-actions[bot]
+ CI_COMMIT_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+ BRANCH_NAME: openapi-${{ inputs.version }}
+
+jobs:
+ create-doc-and-openapiyaml:
+ name: Generate documentation
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ defaults:
+ run:
+ shell: pwsh
+ steps:
+ - name: Checkout the Repo
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Git create branch
+ run: |
+ git checkout -b ${{ env.BRANCH_NAME }}
+ git push --set-upstream origin ${{ env.BRANCH_NAME }}
+
+ - name: Install Swashbuckle CLI
+ run: dotnet tool install Swashbuckle.AspNetCore.Cli --version 6.6.2 --create-manifest-if-needed
+
+ - name: Install widdershins CLI
+ run: npm install -g widdershins
+
+ - name: Build and generate YAML and MD files
+ run: |
+ $p = @{
+ Authority = "http://api"
+ IssuerUrl = "https://localhost"
+ DatabaseEngine = "PostgreSql"
+ PathBase = "adminapi"
+ SigningKey = "test"
+ AdminDB = "host=db-admin;port=5432;username=username;password=password;database=EdFi_Admin;Application Name=EdFi.Ods.AdminApi;"
+ SecurityDB = "host=db-admin;port=5432;username=username;password=password;database=EdFi_Security;Application Name=EdFi.Ods.AdminApi;"
+ }
+ ./build.ps1 -APIVersion ${{ inputs.version }} -Configuration Release -DockerEnvValues $p -Command GenerateOpenAPIAndMD
+
+ - name: Git add files
+ run: |
+ git add docs/api-specifications/openapi-yaml/*
+ git add docs/api-specifications/markdown/*
+ git restore Application/EdFi.Ods.AdminApi/appsettings.json
+ git status --porcelain
+
+ - name: Commit file
+ id: commit
+ uses: planetscale/ghcommit-action@d4176bfacef926cc2db351eab20398dfc2f593b5 #v0.2.0
+ with:
+ commit_message: "${{ env.CI_COMMIT_MESSAGE }}"
+ repo: ${{ github.repository }}
+ branch: ${{ env.BRANCH_NAME }}
+ file_pattern: '*.yaml *.md'
+
+ - name: Create PR
+ run: gh pr create -B main -H ${{ env.BRANCH_NAME }} --title '[Github Action] Open API documentation version ${{ inputs.version }}' --body 'Created by Github action'
diff --git a/.gitignore b/.gitignore
index e3dfc902e..d570fb468 100644
--- a/.gitignore
+++ b/.gitignore
@@ -62,5 +62,13 @@ credentials-*.xml
libs/
libs.codegen/
-Docker/Application
-dhparam.pem
+#Docker
+Docker/Application/
+Docker/Settings/ssl/server.crt
+Docker/Settings/ssl/server.key
+Docker/Settings/ssl/dhparam.pem
+
+# Code coverage
+coverage
+coverage.*
+coveragereport/
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 1a2f1c89e..9e2955bea 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -4,6 +4,10 @@
"EditorConfig.EditorConfig",
"ms-dotnettools.csharp",
"ms-vscode.powershell",
- "streetsidesoftware.code-spell-checker"
+ "csharpier.csharpier-vscode",
+ "humao.rest-client",
+ "streetsidesoftware.code-spell-checker",
+ "bierner.markdown-mermaid",
+ "DavidAnson.vscode-markdownlint"
]
-}
\ No newline at end of file
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 348747261..778810af9 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -1,10 +1,14 @@
{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
"configurations": [
{
- "name": "C#: EdFi.Ods.AdminApi [Default Configuration]",
+ "name": "C#: Admin API 2 Debug",
"type": "dotnet",
"request": "launch",
- "projectPath": "${workspaceFolder}\\Application\\EdFi.Ods.AdminApi\\EdFi.Ods.AdminApi.csproj"
+ "projectPath": "${workspaceFolder}/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj"
}
]
-}
+
diff --git a/.vscode/settings.json b/.vscode/settings.json
index e18f2117c..8721e71f7 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -18,7 +18,7 @@
]
}
],
- "psi-header.lang-config":[
+ "psi-header.lang-config": [
{
"_": "Apply double slash-based comments",
"language": "csharp",
@@ -67,8 +67,31 @@
],
"psi-header.changes-tracking": {
"autoHeader": "autoSave",
- "exclude": [
- "css", "json", "xml", "config", "plaintext", "markdown", "batch"
- ]
- }
-}
\ No newline at end of file
+ "exclude": ["css", "json", "xml", "config", "plaintext", "markdown", "batch"]
+ },
+ "dotnet.defaultSolution": "Application/Ed-Fi-ODS-AdminApi.sln",
+ "markdownlint.config": {
+ "default": true,
+ "line-length": false,
+ "no-inline-html": {
+ "allowed_elements": ["kbd", "details", "summary", "br", "iframe", "sup"]
+ },
+ "emphasis-style": {
+ "style": "underscore"
+ },
+ "ul-style": {
+ "style": "asterisk"
+ }
+ },
+ "[markdown]": {
+ "editor.formatOnSave": true,
+ "editor.formatOnPaste": true,
+ "editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
+ },
+ "editor.codeActionsOnSave": {
+ "source.fixAll.markdownlint": "explicit"
+ },
+ "cSpell.words": [
+ "Shouldly"
+ ]
+}
diff --git a/Application/Directory.Packages.props b/Application/Directory.Packages.props
new file mode 100644
index 000000000..bb56bd234
--- /dev/null
+++ b/Application/Directory.Packages.props
@@ -0,0 +1,101 @@
+
+
+ true
+
+
+ $(NoWarn);NU1507;NU1604;NU1602;NU1603;NU1701;NU1902;NU1903
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Application/Ed-Fi-ODS-AdminApi.sln b/Application/Ed-Fi-ODS-AdminApi.sln
index 82ca90a56..b1bc7f739 100644
--- a/Application/Ed-Fi-ODS-AdminApi.sln
+++ b/Application/Ed-Fi-ODS-AdminApi.sln
@@ -1,65 +1,108 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.5.33516.290
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1D317139-CB19-4D7F-91AC-9A560189C55B}"
- ProjectSection(SolutionItems) = preProject
- ..\CONTRIBUTORS.md = ..\CONTRIBUTORS.md
- ..\NOTICES.md = ..\NOTICES.md
- ..\README.md = ..\README.md
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnitTests", "UnitTests", "{9A9D18B4-718D-4681-BAFE-A1C42E18A7CC}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdFi.Ods.AdminApi.UnitTests", "EdFi.Ods.AdminApi.UnitTests\EdFi.Ods.AdminApi.UnitTests.csproj", "{F62C9CF6-A632-4894-B61A-674198DAB86E}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdFi.Ods.AdminApi", "EdFi.Ods.AdminApi\EdFi.Ods.AdminApi.csproj", "{FBFFA828-316C-43C8-B52A-4D64C51DF6EB}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IntegrationTests", "IntegrationTests", "{D8A26B59-6DAD-4046-9DDE-00D2CFDAE9B6}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdFi.Ods.AdminApi.DBTests", "EdFi.Ods.AdminApi.DBTests\EdFi.Ods.AdminApi.DBTests.csproj", "{73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Debug|x64 = Debug|x64
- Release|Any CPU = Release|Any CPU
- Release|x64 = Release|x64
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|x64.ActiveCfg = Debug|Any CPU
- {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|x64.Build.0 = Debug|Any CPU
- {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|Any CPU.Build.0 = Release|Any CPU
- {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|x64.ActiveCfg = Release|Any CPU
- {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|x64.Build.0 = Release|Any CPU
- {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|x64.ActiveCfg = Debug|Any CPU
- {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|x64.Build.0 = Debug|Any CPU
- {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|Any CPU.Build.0 = Release|Any CPU
- {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|x64.ActiveCfg = Release|Any CPU
- {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|x64.Build.0 = Release|Any CPU
- {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|x64.ActiveCfg = Debug|Any CPU
- {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|x64.Build.0 = Debug|Any CPU
- {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|Any CPU.Build.0 = Release|Any CPU
- {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|x64.ActiveCfg = Release|Any CPU
- {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|x64.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {F62C9CF6-A632-4894-B61A-674198DAB86E} = {9A9D18B4-718D-4681-BAFE-A1C42E18A7CC}
- {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5} = {D8A26B59-6DAD-4046-9DDE-00D2CFDAE9B6}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {30CF2BE4-58CA-4598-9B59-D334FC971A0F}
- EndGlobalSection
-EndGlobal
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.33516.290
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1D317139-CB19-4D7F-91AC-9A560189C55B}"
+ ProjectSection(SolutionItems) = preProject
+ ..\CONTRIBUTORS.md = ..\CONTRIBUTORS.md
+ ..\NOTICES.md = ..\NOTICES.md
+ ..\README.md = ..\README.md
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnitTests", "UnitTests", "{9A9D18B4-718D-4681-BAFE-A1C42E18A7CC}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdFi.Ods.AdminApi.UnitTests", "EdFi.Ods.AdminApi.UnitTests\EdFi.Ods.AdminApi.UnitTests.csproj", "{F62C9CF6-A632-4894-B61A-674198DAB86E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdFi.Ods.AdminApi", "EdFi.Ods.AdminApi\EdFi.Ods.AdminApi.csproj", "{FBFFA828-316C-43C8-B52A-4D64C51DF6EB}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IntegrationTests", "IntegrationTests", "{D8A26B59-6DAD-4046-9DDE-00D2CFDAE9B6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdFi.Ods.AdminApi.DBTests", "EdFi.Ods.AdminApi.DBTests\EdFi.Ods.AdminApi.DBTests.csproj", "{73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.Common", "EdFi.Ods.AdminApi.Common\EdFi.Ods.AdminApi.Common.csproj", "{C9C86866-562B-4EA3-9AAC-F3297F0754D6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.Common.UnitTests", "EdFi.Ods.AdminApi.Common.UnitTests\EdFi.Ods.AdminApi.Common.UnitTests.csproj", "{D0B566A2-000E-48FC-9CD0-702F7A00CA76}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.V1", "EdFi.Ods.AdminApi.V1\EdFi.Ods.AdminApi.V1.csproj", "{760F71C6-9FD9-4F5B-AE77-34501EE76FBD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.V1.DBTests", "EdFi.Ods.AdminApi.V1.DBTests\EdFi.Ods.AdminApi.V1.DBTests.csproj", "{4377A3CF-71EC-4097-AC8C-789ED63075C9}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|x64.Build.0 = Debug|Any CPU
+ {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|x64.ActiveCfg = Release|Any CPU
+ {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|x64.Build.0 = Release|Any CPU
+ {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|x64.Build.0 = Debug|Any CPU
+ {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|x64.ActiveCfg = Release|Any CPU
+ {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|x64.Build.0 = Release|Any CPU
+ {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|x64.Build.0 = Debug|Any CPU
+ {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|x64.ActiveCfg = Release|Any CPU
+ {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|x64.Build.0 = Release|Any CPU
+ {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Debug|x64.Build.0 = Debug|Any CPU
+ {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Release|x64.ActiveCfg = Release|Any CPU
+ {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Release|x64.Build.0 = Release|Any CPU
+ {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Debug|x64.Build.0 = Debug|Any CPU
+ {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Release|x64.ActiveCfg = Release|Any CPU
+ {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Release|x64.Build.0 = Release|Any CPU
+ {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Debug|x64.Build.0 = Debug|Any CPU
+ {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Release|x64.ActiveCfg = Release|Any CPU
+ {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Release|x64.Build.0 = Release|Any CPU
+ {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Debug|x64.Build.0 = Debug|Any CPU
+ {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Release|x64.ActiveCfg = Release|Any CPU
+ {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Release|x64.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {1D317139-CB19-4D7F-91AC-9A560189C55B} = {D8A26B59-6DAD-4046-9DDE-00D2CFDAE9B6}
+ {F62C9CF6-A632-4894-B61A-674198DAB86E} = {9A9D18B4-718D-4681-BAFE-A1C42E18A7CC}
+ {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5} = {D8A26B59-6DAD-4046-9DDE-00D2CFDAE9B6}
+ {4377A3CF-71EC-4097-AC8C-789ED63075C9} = {D8A26B59-6DAD-4046-9DDE-00D2CFDAE9B6}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {30CF2BE4-58CA-4598-9B59-D334FC971A0F}
+ EndGlobalSection
+EndGlobal
diff --git a/Application/EdFi.Ods.AdminApi.Common.UnitTests/EdFi.Ods.AdminApi.Common.UnitTests.csproj b/Application/EdFi.Ods.AdminApi.Common.UnitTests/EdFi.Ods.AdminApi.Common.UnitTests.csproj
new file mode 100644
index 000000000..5d29f9027
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common.UnitTests/EdFi.Ods.AdminApi.Common.UnitTests.csproj
@@ -0,0 +1,36 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Application/EdFi.Ods.AdminApi.Common.UnitTests/Features/AdminApiErrorTest.cs b/Application/EdFi.Ods.AdminApi.Common.UnitTests/Features/AdminApiErrorTest.cs
new file mode 100644
index 000000000..2c1e7b748
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common.UnitTests/Features/AdminApiErrorTest.cs
@@ -0,0 +1,337 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using EdFi.Ods.AdminApi.Common.Features;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Shouldly;
+using Results = FluentValidation.Results;
+
+namespace EdFi.Ods.AdminApi.Common.UnitTests.Features;
+
+[TestFixture]
+public class AdminApiErrorTest
+{
+ [Test]
+ public void Validation_ShouldReturnValidationProblem()
+ {
+ // Arrange
+ var validationFailures = new List
+ {
+ new("Property1", "Error1"),
+ new("Property1", "Error2"),
+ new("Property2", "Error3")
+ };
+
+ // Act
+ var result = AdminApiError.Validation(validationFailures);
+
+ // Assert
+ result.ShouldBeOfType();
+ }
+
+ [Test]
+ public void Validation_ShouldNotBeNull()
+ {
+ // Arrange
+ var validationFailures = new List
+ {
+ new("Property1", "Error1"),
+ new("Property1", "Error2"),
+ new("Property2", "Error3")
+ };
+
+ // Act
+ var result = AdminApiError.Validation(validationFailures);
+
+ // Assert
+ var validationProblemDetails = result as ProblemHttpResult;
+ validationProblemDetails.ShouldNotBeNull();
+ }
+
+ [Test]
+ public void Validation_ShouldContainErrors()
+ {
+ // Arrange
+ var validationFailures = new List
+ {
+ new("Property1", "Error1"),
+ new("Property1", "Error2"),
+ new("Property2", "Error3")
+ };
+
+ // Act
+ var result = AdminApiError.Validation(validationFailures);
+
+ // Assert
+ var details = (result as ProblemHttpResult)?.ProblemDetails as HttpValidationProblemDetails;
+ details.ShouldNotBeNull();
+ details.Errors.ShouldContainKey("Property1");
+ details.Errors["Property1"].ShouldBeEquivalentTo(new[] { "Error1", "Error2" });
+ details.Errors.ShouldContainKey("Property2");
+ details.Errors["Property2"].ShouldBeEquivalentTo(new[] { "Error3" });
+ }
+
+ [Test]
+ public void Unexpected_ShouldReturnProblemWithMessage()
+ {
+ // Arrange
+ var message = "Unexpected error occurred";
+
+ // Act
+ var result = AdminApiError.Unexpected(message);
+
+ // Assert
+ result.ShouldBeOfType();
+ }
+
+ [Test]
+ public void Unexpected_ShouldNotBeNull()
+ {
+ // Arrange
+ var message = "Unexpected error occurred";
+
+ // Act
+ var result = AdminApiError.Unexpected(message);
+
+ // Assert
+ var problemDetails = result as ProblemHttpResult;
+ problemDetails.ShouldNotBeNull();
+ }
+
+ [Test]
+ public void Unexpected_ShouldHaveCorrectTitle()
+ {
+ // Arrange
+ var message = "Unexpected error occurred";
+
+ // Act
+ var result = AdminApiError.Unexpected(message);
+
+ // Assert
+ var problemDetails = result as ProblemHttpResult;
+ problemDetails?.ProblemDetails.Title.ShouldBe(message);
+ }
+
+ [Test]
+ public void Unexpected_ShouldHaveCorrectStatus()
+ {
+ // Arrange
+ var message = "Unexpected error occurred";
+
+ // Act
+ var result = AdminApiError.Unexpected(message);
+
+ // Assert
+ var problemDetails = result as ProblemHttpResult;
+ problemDetails?.ProblemDetails.Status.ShouldBe(500);
+ }
+
+ [Test]
+ public void Unexpected_WithErrors_ShouldReturnProblemWithMessageAndErrors()
+ {
+ // Arrange
+ var message = "Unexpected error occurred";
+ var errors = new[] { "Error1", "Error2" };
+
+ // Act
+ var result = AdminApiError.Unexpected(message, errors);
+
+ // Assert
+ result.ShouldBeOfType();
+ }
+
+ [Test]
+ public void Unexpected_WithErrors_ShouldNotBeNull()
+ {
+ // Arrange
+ var message = "Unexpected error occurred";
+ var errors = new[] { "Error1", "Error2" };
+
+ // Act
+ var result = AdminApiError.Unexpected(message, errors);
+
+ // Assert
+ var problemDetails = result as ProblemHttpResult;
+ problemDetails.ShouldNotBeNull();
+ }
+
+ [Test]
+ public void Unexpected_WithErrors_ShouldHaveCorrectTitle()
+ {
+ // Arrange
+ var message = "Unexpected error occurred";
+ var errors = new[] { "Error1", "Error2" };
+
+ // Act
+ var result = AdminApiError.Unexpected(message, errors);
+
+ // Assert
+ var problemDetails = result as ProblemHttpResult;
+ problemDetails?.ProblemDetails.Title.ShouldBe(message);
+ }
+
+ [Test]
+ public void Unexpected_WithErrors_ShouldHaveCorrectStatus()
+ {
+ // Arrange
+ var message = "Unexpected error occurred";
+ var errors = new[] { "Error1", "Error2" };
+
+ // Act
+ var result = AdminApiError.Unexpected(message, errors);
+
+ // Assert
+ var problemDetails = result as ProblemHttpResult;
+ problemDetails?.ProblemDetails.Status.ShouldBe(500);
+ }
+
+ [Test]
+ public void Unexpected_WithErrors_ShouldHaveCorrectExtensions()
+ {
+ // Arrange
+ var message = "Unexpected error occurred";
+ var errors = new[] { "Error1", "Error2" };
+
+ // Act
+ var result = AdminApiError.Unexpected(message, errors);
+
+ // Assert
+ var problemDetails = result as ProblemHttpResult;
+ problemDetails?.ProblemDetails.Extensions["errors"].ShouldBeEquivalentTo(errors);
+ }
+
+ [Test]
+ public void Unexpected_WithException_ShouldReturnProblemWithExceptionMessage()
+ {
+ // Arrange
+ var exception = new Exception("Exception message");
+
+ // Act
+ var result = AdminApiError.Unexpected(exception);
+
+ // Assert
+ result.ShouldBeOfType();
+ }
+
+ [Test]
+ public void Unexpected_WithException_ShouldNotBeNull()
+ {
+ // Arrange
+ var exception = new Exception("Exception message");
+
+ // Act
+ var result = AdminApiError.Unexpected(exception);
+
+ // Assert
+ var problemDetails = result as ProblemHttpResult;
+ problemDetails.ShouldNotBeNull();
+ }
+
+ [Test]
+ public void Unexpected_WithException_ShouldHaveCorrectTitle()
+ {
+ // Arrange
+ var exception = new Exception("Exception message");
+
+ // Act
+ var result = AdminApiError.Unexpected(exception);
+
+ // Assert
+ var problemDetails = result as ProblemHttpResult;
+ problemDetails?.ProblemDetails.Title.ShouldBe(exception.Message);
+ }
+
+ [Test]
+ public void Unexpected_WithException_ShouldHaveCorrectStatus()
+ {
+ // Arrange
+ var exception = new Exception("Exception message");
+
+ // Act
+ var result = AdminApiError.Unexpected(exception);
+
+ // Assert
+ var problemDetails = result as ProblemHttpResult;
+ problemDetails?.ProblemDetails.Status.ShouldBe(500);
+ }
+
+ [Test]
+ public void NotFound_ShouldReturnNotFoundWithResourceNameAndId()
+ {
+ // Arrange
+ var resourceName = "Resource";
+ var id = 123;
+
+ // Act
+ var result = AdminApiError.NotFound(resourceName, id);
+
+ // Assert
+ result.ShouldBeOfType>();
+ }
+
+ [Test]
+ public void NotFound_ShouldNotBeNull()
+ {
+ // Arrange
+ var resourceName = "Resource";
+ var id = 123;
+
+ // Act
+ var result = AdminApiError.NotFound(resourceName, id);
+
+ // Assert
+ var notFoundResult = result as NotFound;
+ notFoundResult.ShouldNotBeNull();
+ }
+
+ [Test]
+ public void NotFound_ShouldHaveCorrectValue()
+ {
+ // Arrange
+ var resourceName = "Resource";
+ var id = 123;
+
+ // Act
+ var result = AdminApiError.NotFound(resourceName, id);
+
+ // Assert
+ var notFoundResult = result as NotFound;
+ notFoundResult?.Value.ShouldBe($"Not found: {resourceName} with ID {id}");
+ }
+
+ [Test]
+ public void Validation_WithEmptyErrors_ShouldReturnValidationProblem()
+ {
+ // Act
+ var result = AdminApiError.Validation(new List());
+
+ // Assert
+ result.ShouldBeOfType();
+ }
+
+ [Test]
+ public void Validation_WithEmptyErrors_ShouldNotBeNull()
+ {
+ // Act
+ var result = AdminApiError.Validation(new List());
+
+ // Assert
+ var validationProblemDetails = result as ProblemHttpResult;
+ validationProblemDetails.ShouldNotBeNull();
+ }
+
+ [Test]
+ public void Validation_WithEmptyErrors_ShouldHaveEmptyErrors()
+ {
+ // Act
+ var result = AdminApiError.Validation(new List());
+
+ // Assert
+ var details = (result as ProblemHttpResult)?.ProblemDetails as HttpValidationProblemDetails;
+ details.ShouldNotBeNull();
+ details.Errors.ShouldBeEmpty();
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs b/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs
new file mode 100644
index 000000000..36c03fb1a
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+namespace EdFi.Ods.AdminApi.Common.Constants;
+
+public class Constants
+{
+ public const string TenantsCacheKey = "tenants";
+ public const string TenantNameDescription = "Admin API Tenant Name";
+ public const string TenantConnectionStringDescription = "Tenant connection strings";
+ public const string DefaultTenantName = "default";
+}
+
+public enum AdminApiMode
+{
+ V2,
+ V1,
+ Unversioned
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/EdFi.Ods.AdminApi.Common.csproj b/Application/EdFi.Ods.AdminApi.Common/EdFi.Ods.AdminApi.Common.csproj
new file mode 100644
index 000000000..2c1d0c024
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/EdFi.Ods.AdminApi.Common.csproj
@@ -0,0 +1,37 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Application/EdFi.Ods.AdminApi.Common/Features/AdminApiError.cs b/Application/EdFi.Ods.AdminApi.Common/Features/AdminApiError.cs
new file mode 100644
index 000000000..798c97ede
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Features/AdminApiError.cs
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using FluentValidation.Results;
+using Microsoft.AspNetCore.Http;
+using Swashbuckle.AspNetCore.Annotations;
+
+namespace EdFi.Ods.AdminApi.Common.Features;
+
+[SwaggerSchema(Title = "AdminApiError", Description = "Wrapper schema for all error responses")]
+public static class AdminApiError
+{
+ public static IResult Validation(IEnumerable errors) =>
+ Results.ValidationProblem(
+ errors
+ .GroupBy(e => e.PropertyName)
+ .ToDictionary(g => g.Key, g => g.Select(f => f.ErrorMessage).ToArray())
+ );
+
+ public static IResult Unexpected(string message) => Results.Problem(statusCode: 500, title: message);
+
+ public static IResult Unexpected(string message, IEnumerable errors) =>
+ Results.Problem(
+ statusCode: 500,
+ title: message,
+ extensions: new Dictionary { { "errors", errors } }
+ );
+
+ public static IResult Unexpected(Exception exception) =>
+ Results.Problem(statusCode: 500, title: exception.Message);
+
+ public static IResult NotFound(string resourceName, T id) =>
+ Results.NotFound($"Not found: {resourceName} with ID {id}");
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Features/FeatureCommonConstants.cs b/Application/EdFi.Ods.AdminApi.Common/Features/FeatureCommonConstants.cs
new file mode 100644
index 000000000..441beff47
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Features/FeatureCommonConstants.cs
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+namespace EdFi.Ods.AdminApi.Common.Features;
+
+public static class FeatureCommonConstants
+{
+ public const string DeletedSuccessResponseDescription = "Resource was successfully deleted.";
+ public const string InternalServerErrorResponseDescription = "Internal server error. An unhandled error occurred on the server. See the response body for details.";
+ public const string BadRequestResponseDescription = "Bad Request. The request was invalid and cannot be completed. See the response body for details.";
+}
diff --git a/Application/EdFi.Ods.AdminApi/Features/IFeature.cs b/Application/EdFi.Ods.AdminApi.Common/Features/IFeature.cs
similarity index 81%
rename from Application/EdFi.Ods.AdminApi/Features/IFeature.cs
rename to Application/EdFi.Ods.AdminApi.Common/Features/IFeature.cs
index 7f10c4402..52e42d6a6 100644
--- a/Application/EdFi.Ods.AdminApi/Features/IFeature.cs
+++ b/Application/EdFi.Ods.AdminApi.Common/Features/IFeature.cs
@@ -3,7 +3,9 @@
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.
-namespace EdFi.Ods.AdminApi.Features;
+using Microsoft.AspNetCore.Routing;
+
+namespace EdFi.Ods.AdminApi.Common.Features;
public interface IFeature
{
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiEndpointBuilder.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiEndpointBuilder.cs
new file mode 100644
index 000000000..3b17abaa4
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiEndpointBuilder.cs
@@ -0,0 +1,193 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using System.Data;
+using EdFi.Ods.AdminApi.Common.Features;
+using EdFi.Ods.AdminApi.Common.Infrastructure.Extensions;
+using EdFi.Ods.AdminApi.Common.Infrastructure.Security;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Routing;
+using Swashbuckle.AspNetCore.Annotations;
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure;
+
+public class AdminApiEndpointBuilder
+{
+ private AdminApiEndpointBuilder(IEndpointRouteBuilder endpoints,
+ HttpVerb verb, string route, Delegate handler)
+ {
+ _endpoints = endpoints;
+ _verb = verb;
+ _route = route.Trim('/');
+ _handler = handler;
+ _pluralResourceName = _route.Split('/')[0];
+ }
+
+ private readonly IEndpointRouteBuilder _endpoints;
+ private readonly HttpVerb? _verb;
+ private readonly string _route;
+ private readonly Delegate? _handler;
+ private readonly List> _routeOptions = [];
+ private readonly string _pluralResourceName;
+ private bool _allowAnonymous = false;
+ private IEnumerable _authorizationPolicies = [];
+
+ public static AdminApiEndpointBuilder MapGet(IEndpointRouteBuilder endpoints, string route, Delegate handler)
+ => new(endpoints, HttpVerb.GET, route, handler);
+
+ public static AdminApiEndpointBuilder MapPost(IEndpointRouteBuilder endpoints, string route, Delegate handler)
+ => new(endpoints, HttpVerb.POST, route, handler);
+
+ public static AdminApiEndpointBuilder MapPut(IEndpointRouteBuilder endpoints, string route, Delegate handler)
+ => new(endpoints, HttpVerb.PUT, route, handler);
+
+ public static AdminApiEndpointBuilder MapDelete(IEndpointRouteBuilder endpoints, string route, Delegate handler)
+ => new(endpoints, HttpVerb.DELETE, route, handler);
+
+ ///
+ /// Includes the specified authorization policy in the endpoint.
+ ///
+ /// List of authorization Policies to validate
+ ///
+ public AdminApiEndpointBuilder RequireAuthorization(IEnumerable authorizationPolicies)
+ {
+ _authorizationPolicies = authorizationPolicies.Select(policy => policy.PolicyName).ToList();
+ return this;
+ }
+
+ ///
+ /// Includes the specified authorization policy in the endpoint.
+ ///
+ /// List of authorization Policies to validate
+ ///
+ public AdminApiEndpointBuilder RequireAuthorization(PolicyDefinition authorizationPolicies)
+ {
+ _authorizationPolicies = [authorizationPolicies.PolicyName];
+ return this;
+ }
+
+ public void BuildForVersions(params AdminApiVersions.AdminApiVersion[] versions)
+ {
+ BuildForVersions(string.Empty, false, versions);
+ }
+
+ public void BuildForVersions(string authorizationPolicy, bool display409 = false, params AdminApiVersions.AdminApiVersion[] versions)
+ {
+ if (versions.Length == 0)
+ throw new ArgumentException("Must register for at least 1 version");
+ if (_route == null)
+ throw new InvalidOperationException("Invalid endpoint registration. Route must be specified");
+ if (_handler == null)
+ throw new InvalidOperationException("Invalid endpoint registration. Handler must be specified");
+
+ foreach (var version in versions)
+ {
+ if (version == null)
+ throw new ArgumentException("Version cannot be null");
+
+ var versionedRoute = $"/{version}/{_route}";
+
+ var builder = _verb switch
+ {
+ HttpVerb.GET => _endpoints.MapGet(versionedRoute, _handler),
+ HttpVerb.POST => _endpoints.MapPost(versionedRoute, _handler),
+ HttpVerb.PUT => _endpoints.MapPut(versionedRoute, _handler),
+ HttpVerb.DELETE => _endpoints.MapDelete(versionedRoute, _handler),
+ _ => throw new ArgumentOutOfRangeException($"Unconfigured HTTP verb for mapping: {_verb}")
+ };
+
+ if (_allowAnonymous)
+ {
+ builder.AllowAnonymous();
+ }
+ else
+ {
+ if (_authorizationPolicies.Any())
+ {
+ builder.RequireAuthorization(_authorizationPolicies.ToArray());
+ }
+ else if (!string.IsNullOrWhiteSpace(authorizationPolicy))
+ {
+ builder.RequireAuthorization(authorizationPolicy);
+ }
+ else
+ {
+ builder.RequireAuthorization();
+ }
+ }
+
+ builder.WithGroupName(version.ToString());
+ builder.WithResponseCode(401, "Unauthorized. The request requires authentication");
+ builder.WithResponseCode(403, "Forbidden. The request is authenticated, but not authorized to access this resource");
+ if (display409)
+ builder.WithResponseCode(409, "Conflict. The request is authenticated, but it has a conflict with an existing element");
+ builder.WithResponseCode(500, FeatureCommonConstants.InternalServerErrorResponseDescription);
+
+ if (_route.Contains("id", StringComparison.InvariantCultureIgnoreCase))
+ {
+ builder.WithResponseCode(404, "Not found. A resource with given identifier could not be found.");
+ }
+
+ if (_verb is HttpVerb.PUT or HttpVerb.POST)
+ {
+ builder.WithResponseCode(400, FeatureCommonConstants.BadRequestResponseDescription);
+ }
+
+ foreach (var action in _routeOptions)
+ {
+ action(builder);
+ }
+ }
+ }
+
+ public AdminApiEndpointBuilder WithRouteOptions(Action routeHandlerBuilderAction)
+ {
+ _routeOptions.Add(routeHandlerBuilderAction);
+ return this;
+ }
+
+ public AdminApiEndpointBuilder WithDefaultSummaryAndDescription()
+ {
+ var summary = _verb switch
+ {
+ HttpVerb.GET => _route.Contains("id") ? $"Retrieves a specific {_pluralResourceName.ToSingleEntity()} based on the identifier." : $"Retrieves all {_pluralResourceName}.",
+ HttpVerb.POST => $"Creates {_pluralResourceName.ToSingleEntity()} based on the supplied values.",
+ HttpVerb.PUT => $"Updates {_pluralResourceName.ToSingleEntity()} based on the resource identifier.",
+ HttpVerb.DELETE => $"Deletes an existing {_pluralResourceName.ToSingleEntity()} using the resource identifier.",
+ _ => throw new ArgumentOutOfRangeException($"Unconfigured HTTP verb for default description {_verb}")
+ };
+
+ var description = _verb switch
+ {
+ HttpVerb.GET => "This GET operation provides access to resources using the \"Get\" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists).",
+ HttpVerb.POST => "The POST operation can be used to create or update resources. In database terms, this is often referred to as an \"upsert\" operation (insert + update). Clients should NOT include the resource \"id\" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.",
+ HttpVerb.PUT => "The PUT operation is used to update a resource by identifier. If the resource identifier (\"id\") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.",
+ HttpVerb.DELETE => "The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found).",
+ _ => throw new ArgumentOutOfRangeException($"Unconfigured HTTP verb for default description {_verb}")
+ };
+
+ return WithSummaryAndDescription(summary, description);
+ }
+
+ public AdminApiEndpointBuilder WithSummary(string summary)
+ {
+ _routeOptions.Add(rhb => rhb.WithMetadata(new SwaggerOperationAttribute(summary)));
+ return this;
+ }
+
+ public AdminApiEndpointBuilder WithSummaryAndDescription(string summary, string description)
+ {
+ _routeOptions.Add(rhb => rhb.WithMetadata(new SwaggerOperationAttribute(summary, description)));
+ return this;
+ }
+
+ public AdminApiEndpointBuilder AllowAnonymous()
+ {
+ _allowAnonymous = true;
+ return this;
+ }
+
+ private enum HttpVerb { GET, POST, PUT, DELETE }
+}
diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiVersions.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiVersions.cs
similarity index 69%
rename from Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiVersions.cs
rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiVersions.cs
index bfdfb8d60..c00e4bf76 100644
--- a/Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiVersions.cs
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiVersions.cs
@@ -6,22 +6,27 @@
using System.Reflection;
using Asp.Versioning.Builder;
using Asp.Versioning.Conventions;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.Configuration;
-namespace EdFi.Ods.AdminApi.Infrastructure;
+namespace EdFi.Ods.AdminApi.Common.Infrastructure;
public class AdminApiVersions
{
private static bool _isInitialized;
- public static AdminApiVersion V1 = new(1.1, "v1");
+ public static readonly AdminApiVersion V1 = new(1.1, "v1");
+ public static readonly AdminApiVersion V2 = new(2.0, "v2");
private static ApiVersionSet? _versionSet;
public static void Initialize(WebApplication app)
{
- if (_isInitialized) throw new InvalidOperationException("Versions are already initialized");
+ if (_isInitialized)
+ throw new InvalidOperationException("Versions are already initialized");
_versionSet = app.NewApiVersionSet()
.HasApiVersion(V1.Version)
+ .HasApiVersion(V2.Version)
.Build();
_isInitialized = true;
@@ -29,7 +34,7 @@ public static void Initialize(WebApplication app)
public static ApiVersionSet VersionSet
{
- get => _versionSet ?? throw new Exception(
+ get => _versionSet ?? throw new ArgumentException(
"Admin API Versions have not been initialized. Call Initialize() at app startup");
}
@@ -50,16 +55,10 @@ public static string[] GetAllVersionStrings()
.ToArray();
}
- public class AdminApiVersion
+ public class AdminApiVersion(double version, string displayName)
{
- public double Version { get; }
- public string DisplayName { get; }
+ public double Version { get; } = version;
+ public string DisplayName { get; } = displayName;
public override string ToString() => DisplayName;
-
- public AdminApiVersion(double version, string displayName)
- {
- Version = version;
- DisplayName = displayName;
- }
}
}
diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/CloudOdsAdminApp.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/CloudOdsAdminApp.cs
similarity index 95%
rename from Application/EdFi.Ods.AdminApi/Infrastructure/CloudOdsAdminApp.cs
rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/CloudOdsAdminApp.cs
index 6e974e19d..ec34dc92b 100644
--- a/Application/EdFi.Ods.AdminApi/Infrastructure/CloudOdsAdminApp.cs
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/CloudOdsAdminApp.cs
@@ -3,7 +3,7 @@
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.
-namespace EdFi.Ods.AdminApi.Infrastructure;
+namespace EdFi.Ods.AdminApi.Common.Infrastructure;
public static class Constants
{
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/CommonQueryParams.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/CommonQueryParams.cs
new file mode 100644
index 000000000..f32e833ed
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/CommonQueryParams.cs
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure;
+
+public struct CommonQueryParams
+{
+ [FromQuery(Name = "offset")]
+ public int? Offset { get; set; }
+ [FromQuery(Name = "limit")]
+ public int? Limit { get; set; }
+ [FromQuery(Name = "orderBy")]
+ public string? OrderBy { get; set; }
+ [FromQuery(Name = "direction")]
+ public string? Direction { get; set; }
+ [BindNever]
+ public readonly bool IsDescending => SortingDirectionHelper.IsDescendingSorting(Direction);
+ public CommonQueryParams() { }
+ public CommonQueryParams(int? offset, int? limit)
+ {
+ Offset = offset;
+ Limit = limit;
+ }
+ public CommonQueryParams(int? offset, int? limit, string? orderBy, string? direction)
+ {
+ Offset = offset;
+ Limit = limit;
+ OrderBy = orderBy;
+ Direction = SortingDirectionHelper.GetNonEmptyOrDefault(direction);
+ }
+
+}
+
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Context/ContextProvider.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Context/ContextProvider.cs
new file mode 100644
index 000000000..8db7bc20f
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Context/ContextProvider.cs
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Context;
+
+public interface IContextProvider
+{
+ T? Get();
+ void Set(T? context);
+}
+
+public class ContextProvider(IContextStorage contextStorage) : IContextProvider
+{
+ private static readonly string? _contextKey = typeof(T?).FullName;
+
+ private readonly IContextStorage _contextStorage = contextStorage;
+
+ public T? Get() => _contextStorage.GetValue(_contextKey!);
+
+ public void Set(T? context) => _contextStorage.SetValue(_contextKey!, context!);
+}
+
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Context/ContextStorage.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Context/ContextStorage.cs
new file mode 100644
index 000000000..8cd81f290
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Context/ContextStorage.cs
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using System.Collections;
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Context
+{
+ public interface IContextStorage
+ {
+ void SetValue(string key, object value);
+
+ T? GetValue(string key);
+ }
+
+ public class HashtableContextStorage : IContextStorage
+ {
+
+ public Hashtable UnderlyingHashtable { get; } = [];
+
+ public void SetValue(string key, object value)
+ {
+ UnderlyingHashtable[key] = value;
+ }
+
+ public T? GetValue(string key) => (T?)(UnderlyingHashtable != null &&
+ UnderlyingHashtable[key] != null ? UnderlyingHashtable[key] : default(T));
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/AdminConsolePostgresUsersContext.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/AdminConsolePostgresUsersContext.cs
new file mode 100644
index 000000000..3ba048008
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/AdminConsolePostgresUsersContext.cs
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Database;
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/AdminConsoleSqlServerUsersContext.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/AdminConsoleSqlServerUsersContext.cs
new file mode 100644
index 000000000..6f4712a6c
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/AdminConsoleSqlServerUsersContext.cs
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Database;
+
diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/EntityFrameworkCoreDatabaseModelBuilderExtensions.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/EntityFrameworkCoreDatabaseModelBuilderExtensions.cs
similarity index 87%
rename from Application/EdFi.Ods.AdminApi/Infrastructure/Database/EntityFrameworkCoreDatabaseModelBuilderExtensions.cs
rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/EntityFrameworkCoreDatabaseModelBuilderExtensions.cs
index ed36f6269..f1f9a13d9 100644
--- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/EntityFrameworkCoreDatabaseModelBuilderExtensions.cs
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/EntityFrameworkCoreDatabaseModelBuilderExtensions.cs
@@ -3,11 +3,10 @@
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.
-using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
-namespace EdFi.Ods.AdminApi.Infrastructure.Database;
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Database;
public static class EntityFrameworkCoreDatabaseModelBuilderExtensions
{
@@ -20,7 +19,8 @@ public static void ApplyDatabaseServerSpecificConventions(this ModelBuilder mode
foreach (var entity in modelBuilder.Model.GetEntityTypes())
{
- if (entity is null) throw new InvalidOperationException("Entity should not be null");
+ if (entity is null)
+ throw new InvalidOperationException("Entity should not be null");
var tableName = entity.GetTableName() ?? throw new InvalidOperationException($"Entity of type {entity.GetType()} has a null table name");
entity.SetTableName(tableName.ToLowerInvariant());
@@ -29,7 +29,7 @@ public static void ApplyDatabaseServerSpecificConventions(this ModelBuilder mode
{
var tableId = StoreObjectIdentifier.Table(tableName);
var columnName = property.GetColumnName(tableId) ?? property.GetDefaultColumnName(tableId);
- property.SetColumnName(columnName.ToLowerInvariant());
+ property.SetColumnName(columnName?.ToLowerInvariant());
}
foreach (var key in entity.GetKeys())
diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/DatabaseEngineEnum.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/DatabaseEngineEnum.cs
similarity index 60%
rename from Application/EdFi.Ods.AdminApi/Infrastructure/DatabaseEngineEnum.cs
rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/DatabaseEngineEnum.cs
index fd566682d..bc13a7357 100644
--- a/Application/EdFi.Ods.AdminApi/Infrastructure/DatabaseEngineEnum.cs
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/DatabaseEngineEnum.cs
@@ -1,9 +1,15 @@
-namespace EdFi.Ods.AdminApi.Infrastructure;
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure;
public static class DatabaseEngineEnum
{
public const string SqlServer = "SqlServer";
public const string PostgreSql = "PostgreSql";
+ public const string SqlServerCollation = "Latin1_General_100_BIN2_UTF8";
public static string Parse(string value)
{
diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/EndpointRouteBuilderExtensions.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/EndpointRouteBuilderExtensions.cs
similarity index 63%
rename from Application/EdFi.Ods.AdminApi/Infrastructure/EndpointRouteBuilderExtensions.cs
rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/EndpointRouteBuilderExtensions.cs
index 0a1879d2f..d39696a4e 100644
--- a/Application/EdFi.Ods.AdminApi/Infrastructure/EndpointRouteBuilderExtensions.cs
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/EndpointRouteBuilderExtensions.cs
@@ -1,6 +1,13 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
using Swashbuckle.AspNetCore.Annotations;
-namespace EdFi.Ods.AdminApi.Infrastructure;
+namespace EdFi.Ods.AdminApi.Common.Infrastructure;
public static class EndpointRouteBuilderExtensions
{
diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/ErrorHandling/AdminAppException.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/AdminApiException.cs
similarity index 65%
rename from Application/EdFi.Ods.AdminApi/Infrastructure/ErrorHandling/AdminAppException.cs
rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/AdminApiException.cs
index 059b50e57..22cdec064 100644
--- a/Application/EdFi.Ods.AdminApi/Infrastructure/ErrorHandling/AdminAppException.cs
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/AdminApiException.cs
@@ -1,7 +1,11 @@
-using System;
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
using System.Net;
-namespace EdFi.Ods.AdminApi.Infrastructure.ErrorHandling;
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling;
public interface IAdminApiException
{
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/NotFoundException.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/NotFoundException.cs
new file mode 100644
index 000000000..df357a676
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/NotFoundException.cs
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling;
+
+public interface INotFoundException
+{
+ public string Message { get; }
+}
+
+public class NotFoundException(string resourceName, T id) : Exception($"Not found: {resourceName} with ID {id}. It may have been recently deleted."), INotFoundException
+{
+ public string ResourceName { get; } = resourceName;
+ public T Id { get; } = id;
+}
diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/ErrorHandling/OdsApiConnectionException.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/OdsApiConnectionException.cs
similarity index 93%
rename from Application/EdFi.Ods.AdminApi/Infrastructure/ErrorHandling/OdsApiConnectionException.cs
rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/OdsApiConnectionException.cs
index 3cdd0f5bd..6d856b06a 100644
--- a/Application/EdFi.Ods.AdminApi/Infrastructure/ErrorHandling/OdsApiConnectionException.cs
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/OdsApiConnectionException.cs
@@ -3,10 +3,9 @@
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.
-using System;
using System.Net;
-namespace EdFi.Ods.AdminApi.Infrastructure.ErrorHandling;
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling;
public class OdsApiConnectionException : Exception, IAdminApiException
{
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Extensions/IConfigurationExtensions.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Extensions/IConfigurationExtensions.cs
new file mode 100644
index 000000000..1ce1515fe
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Extensions/IConfigurationExtensions.cs
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling;
+using Microsoft.Extensions.Configuration;
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Extensions;
+
+public static class IConfigurationExtensions
+{
+ public static T Get(this IConfiguration configuration, string key, T defaultValue)
+ {
+ return configuration.GetValue(key, defaultValue) ?? defaultValue;
+ }
+
+ public static T Get(this IConfiguration configuration, string key)
+ {
+ return configuration.GetValue(key)
+ ?? throw new AdminApiException($"Unable to load {key} from appSettings");
+ }
+
+ public static string GetConnectionStringByName(this IConfiguration configuration, string name)
+ {
+ return configuration.GetConnectionString(name)
+ ?? throw new AdminApiException($"Missing connection string {name}.");
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Extensions/StringExtensions.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Extensions/StringExtensions.cs
new file mode 100644
index 000000000..4dde39c06
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Extensions/StringExtensions.cs
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using System.Globalization;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Extensions;
+
+public static partial class StringExtensions
+{
+ public static string ToTitleCase(this string input)
+ => CultureInfo.CurrentCulture.TextInfo.ToTitleCase(input);
+
+ public static string ToSingleEntity(this string input)
+ {
+ return input.Remove(input.Length - 1, 1);
+ }
+
+ public static string ToDelimiterSeparated(this IEnumerable inputStrings, string separator = ",")
+ {
+ var listOfStrings = inputStrings.ToList();
+
+ return listOfStrings.Count != 0
+ ? string.Join(separator, listOfStrings)
+ : string.Empty;
+ }
+
+ public static object ToJsonObjectResponseDeleted(this string input)
+ {
+ return new { title = $"{input} deleted successfully" };
+ }
+
+ public static string ToPascalCase(this string input)
+ {
+ var matches = ToPascalCaseRegex().Match(input);
+ var groupWords = matches.Groups["word"];
+
+ var thread = Thread.CurrentThread.CurrentCulture.TextInfo;
+ var stringBuilder = new StringBuilder();
+ foreach (var captureWord in groupWords.Captures.Cast())
+ stringBuilder.Append(thread.ToTitleCase(captureWord.Value.ToLower()));
+ return stringBuilder.ToString();
+ }
+
+ [GeneratedRegex("^(?^[a-z]+|[A-Z]+|[A-Z][a-z]+)+$")]
+ private static partial Regex ToPascalCaseRegex();
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/ConnectionStringHelper.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/ConnectionStringHelper.cs
new file mode 100644
index 000000000..b9ba02d5c
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/ConnectionStringHelper.cs
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using log4net;
+using Microsoft.Data.SqlClient;
+using Npgsql;
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Helpers;
+
+public static class ConnectionStringHelper
+{
+ private static readonly ILog _log = LogManager.GetLogger(typeof(ConnectionStringHelper));
+ public static bool ValidateConnectionString(string databaseEngine, string? connectionString)
+ {
+ bool result = true;
+ if (databaseEngine.Equals(DatabaseEngineEnum.SqlServer, StringComparison.InvariantCultureIgnoreCase))
+ {
+ try
+ {
+ _ = new SqlConnectionStringBuilder(connectionString);
+ }
+ catch (Exception ex)
+ {
+ if (ex is ArgumentException ||
+ ex is FormatException ||
+ ex is KeyNotFoundException)
+ {
+ result = false;
+ _log.Error(ex);
+ }
+ }
+ }
+ else if (databaseEngine.Equals(DatabaseEngineEnum.PostgreSql, StringComparison.InvariantCultureIgnoreCase))
+ {
+ try
+ {
+ _ = new NpgsqlConnectionStringBuilder(connectionString);
+ }
+ catch (ArgumentException ex)
+ {
+ result = false;
+ _log.Error(ex);
+ }
+ }
+ return result;
+ }
+
+ public static (string? Host, string? Database) GetHostAndDatabase(string databaseEngine, string? connectionString)
+ {
+ if (databaseEngine.Equals(DatabaseEngineEnum.SqlServer, StringComparison.InvariantCultureIgnoreCase))
+ {
+ try
+ {
+ var builder = new SqlConnectionStringBuilder(connectionString);
+ return (builder.DataSource, builder.InitialCatalog);
+ }
+ catch (Exception ex) when (
+ ex is ArgumentException or
+ FormatException or
+ KeyNotFoundException)
+ {
+ _log.Error(ex);
+ return (null, null);
+ }
+ }
+ else if (databaseEngine.Equals(DatabaseEngineEnum.PostgreSql, StringComparison.InvariantCultureIgnoreCase))
+ {
+ try
+ {
+ var builder = new NpgsqlConnectionStringBuilder(connectionString);
+ return (builder.Host, builder.Database);
+ }
+ catch (ArgumentException ex)
+ {
+ _log.Error(ex);
+ return (null, null);
+ }
+ }
+
+ return (null, null);
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/FeaturesHelper.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/FeaturesHelper.cs
new file mode 100644
index 000000000..243a91bfb
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/FeaturesHelper.cs
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using EdFi.Ods.AdminApi.Common.Features;
+using System.Reflection;
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Helpers;
+
+public static class FeatureHelper
+{
+ public static List GetFeatures(Assembly executingAssembly)
+ {
+ var featureInterface = typeof(IFeature);
+ var featureImpls = executingAssembly.GetTypes()
+ .Where(p => featureInterface.IsAssignableFrom(p) && p.IsClass);
+
+ var features = new List();
+
+ foreach (var featureImpl in featureImpls)
+ {
+ if (Activator.CreateInstance(featureImpl) is IFeature feature)
+ features.Add(feature);
+ }
+ return features;
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/QueryStringMappingHelper.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/QueryStringMappingHelper.cs
new file mode 100644
index 000000000..193b5221a
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/QueryStringMappingHelper.cs
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using System.Linq.Expressions;
+using FluentValidation;
+using FluentValidation.Results;
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Helpers;
+
+public static class QueryStringMappingHelper
+{
+ public static Expression> GetColumnToOrderBy(this Dictionary>> mapping, string? orderBy)
+ {
+ orderBy ??= string.Empty;
+
+ if (mapping != null)
+ {
+ if (string.IsNullOrEmpty(orderBy))
+ return mapping.First().Value;
+
+ if (!mapping.TryGetValue(orderBy.ToLowerInvariant(), out Expression>? result))
+ {
+ throw new ValidationException([new ValidationFailure(nameof(orderBy), $"The orderBy value {orderBy} is not allowed. The allowed values are {string.Join(",", mapping.Keys)}")]);
+ }
+
+ return result;
+ }
+
+ throw new ArgumentNullException(nameof(mapping));
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/SortingDirectionHelper.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/SortingDirectionHelper.cs
new file mode 100644
index 000000000..fee8d061b
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/SortingDirectionHelper.cs
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using System.ComponentModel;
+using System.Runtime.Serialization;
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Helpers;
+
+public static class SortingDirectionHelper
+{
+ public enum Direction
+ {
+ [Description("Ascending")]
+ [EnumMember(Value = "Asc")]
+ Ascending,
+
+ [Description("Descending")]
+ [EnumMember(Value = "Desc")]
+ Descending
+ }
+
+ public static bool IsDescendingSorting(string? direction)
+ {
+ direction = GetNonEmptyOrDefault(direction);
+
+ bool result = direction.ToLowerInvariant() switch
+ {
+ "asc" or "ascending" => false,
+ "desc" or "descending" => true,
+ _ => false,
+ };
+
+ return result;
+ }
+
+ public static string GetNonEmptyOrDefault(string? direction)
+ {
+ if (!string.IsNullOrEmpty(direction))
+ return direction;
+ return Direction.Ascending.ToString();
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/StringToJsonDocumentConverter.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/StringToJsonDocumentConverter.cs
new file mode 100644
index 000000000..a110192ac
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/StringToJsonDocumentConverter.cs
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Helpers
+{
+ public class StringToJsonDocumentConverter : JsonConverter
+ {
+ public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.GetString();
+
+ public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
+ {
+ JsonDocument jsonDocument = JsonDocument.Parse(value);
+ jsonDocument.WriteTo(writer);
+ }
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/InstanceContext.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/InstanceContext.cs
similarity index 89%
rename from Application/EdFi.Ods.AdminApi/Infrastructure/InstanceContext.cs
rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/InstanceContext.cs
index 4e145cfc5..958489ae1 100644
--- a/Application/EdFi.Ods.AdminApi/Infrastructure/InstanceContext.cs
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/InstanceContext.cs
@@ -3,7 +3,7 @@
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.
-namespace EdFi.Ods.AdminApi.Infrastructure;
+namespace EdFi.Ods.AdminApi.Common.Infrastructure;
public class InstanceContext
{
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantConfiguration.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantConfiguration.cs
new file mode 100644
index 000000000..8c1e6e616
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantConfiguration.cs
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy;
+
+public class TenantConfiguration
+{
+ public string? TenantIdentifier { get; set; }
+
+ public string? AdminConnectionString { get; set; }
+
+ public string? SecurityConnectionString { get; set; }
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantConfigurationProvider.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantConfigurationProvider.cs
new file mode 100644
index 000000000..54f62d369
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantConfigurationProvider.cs
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using EdFi.Ods.AdminApi.Common.Settings;
+using Microsoft.Extensions.Options;
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy;
+
+public interface ITenantConfigurationProvider
+{
+ IDictionary Get();
+}
+
+public class TenantConfigurationProvider : ITenantConfigurationProvider
+{
+ private IDictionary _tenantConfigurationByIdentifier;
+
+ public TenantConfigurationProvider(IOptionsMonitor tenantsConfigurationOptions)
+ {
+ _tenantConfigurationByIdentifier = InitializeTenantsConfiguration(tenantsConfigurationOptions.CurrentValue);
+
+ tenantsConfigurationOptions.OnChange(config =>
+ {
+ var newMap = InitializeTenantsConfiguration(config);
+ Interlocked.Exchange(ref _tenantConfigurationByIdentifier, newMap);
+ });
+ }
+
+ private static Dictionary InitializeTenantsConfiguration(TenantsSection config)
+ {
+ return config.Tenants.ToDictionary(
+ t => t.Key,
+ t => new TenantConfiguration
+ {
+ TenantIdentifier = t.Key,
+ AdminConnectionString = t.Value.ConnectionStrings.GetValueOrDefault("EdFi_Admin"),
+ SecurityConnectionString = t.Value.ConnectionStrings.GetValueOrDefault("EdFi_Security"),
+ },
+ StringComparer.OrdinalIgnoreCase);
+ }
+
+ public IDictionary Get() => _tenantConfigurationByIdentifier;
+}
+
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantIdentificationMiddleware.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantIdentificationMiddleware.cs
new file mode 100644
index 000000000..83ddfd1f3
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantIdentificationMiddleware.cs
@@ -0,0 +1,123 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using System.Text.RegularExpressions;
+using EdFi.Ods.AdminApi.Common.Infrastructure.Context;
+using EdFi.Ods.AdminApi.Common.Settings;
+using FluentValidation;
+using FluentValidation.Results;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Options;
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy;
+
+public partial class TenantResolverMiddleware(
+ ITenantConfigurationProvider tenantConfigurationProvider,
+ IContextProvider tenantConfigurationContextProvider,
+ IOptions options,
+ IOptions swaggerOptions) : IMiddleware
+{
+ private readonly ITenantConfigurationProvider _tenantConfigurationProvider = tenantConfigurationProvider;
+ private readonly IContextProvider _tenantConfigurationContextProvider = tenantConfigurationContextProvider;
+ private readonly IOptions _options = options;
+ private readonly IOptions _swaggerOptions = swaggerOptions;
+
+ public async Task InvokeAsync(HttpContext context, RequestDelegate next)
+ {
+ var apiMode = _options.Value.AdminApiMode?.ToLower() ?? "v2";
+ var multiTenancyEnabled = _options.Value.MultiTenancy;
+ var validationErrorMessage = "Please provide valid tenant id. Tenant id can only contain alphanumeric and -";
+
+ // Check if this is a V1 endpoint
+ if (IsV1Mode(apiMode))
+ {
+ // For V1 endpoints, skip multi-tenancy validation entirely
+ await next.Invoke(context);
+ return;
+ }
+ if (multiTenancyEnabled)
+ {
+ if (context.Request.Headers.TryGetValue("tenant", out var tenantIdentifier) &&
+ !string.IsNullOrEmpty(tenantIdentifier))
+ {
+ if (IsValidTenantId(tenantIdentifier!))
+ {
+ if (_tenantConfigurationProvider.Get().TryGetValue(tenantIdentifier!, out var tenantConfiguration))
+ {
+ _tenantConfigurationContextProvider.Set(tenantConfiguration);
+ }
+ else
+ {
+ ThrowTenantValidationError($"Tenant not found with provided tenant id: {tenantIdentifier}");
+ }
+ }
+ else
+ {
+ ThrowTenantValidationError(validationErrorMessage);
+ }
+ }
+ else if (_swaggerOptions.Value.EnableSwagger && RequestFromSwagger())
+ {
+ var defaultTenant = _swaggerOptions.Value.DefaultTenant;
+ if (!string.IsNullOrEmpty(defaultTenant) && IsValidTenantId(defaultTenant))
+ {
+ if (!string.IsNullOrEmpty(defaultTenant) &&
+ _tenantConfigurationProvider.Get().TryGetValue(defaultTenant, out var tenantConfiguration))
+ {
+ _tenantConfigurationContextProvider.Set(tenantConfiguration);
+ }
+ else
+ {
+ ThrowTenantValidationError("Please configure valid default tenant id");
+ }
+ }
+ else
+ {
+ ThrowTenantValidationError(validationErrorMessage);
+ }
+ }
+ else
+ {
+ if (!context.Request.Path.Value!.Contains("adminconsole/tenants") &&
+ context.Request.Method != "GET" &&
+ !context.Request.Path.Value.Contains("health", StringComparison.InvariantCultureIgnoreCase))
+ {
+ ThrowTenantValidationError("Tenant header is missing (adminconsole)");
+ }
+ }
+ }
+ await next.Invoke(context);
+
+ bool RequestFromSwagger() => (context.Request.Path.Value != null &&
+ context.Request.Path.Value.Contains("swagger", StringComparison.InvariantCultureIgnoreCase)) ||
+ context.Request.Headers.Referer.FirstOrDefault(x => x != null && x.ToLower().Contains("swagger", StringComparison.InvariantCultureIgnoreCase)) != null;
+
+ void ThrowTenantValidationError(string errorMessage)
+ {
+ throw new ValidationException([new ValidationFailure("Tenant", errorMessage)]);
+ }
+ }
+
+ private static bool IsV1Mode(string _adminApiMode)
+ {
+ return string.Equals(_adminApiMode, "v1", StringComparison.InvariantCultureIgnoreCase);
+ }
+
+ private static bool IsValidTenantId(string tenantId)
+ {
+ const int MaxLength = 50;
+ var regex = IsValidTenantIdRegex();
+
+ if (string.IsNullOrEmpty(tenantId) || tenantId.Length > MaxLength ||
+ !regex.IsMatch(tenantId))
+ {
+ return false;
+ }
+ return true;
+ }
+
+ [GeneratedRegex("^[A-Za-z0-9-]+$")]
+ private static partial Regex IsValidTenantIdRegex();
+}
diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/OperationalContext.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/OperationalContext.cs
similarity index 79%
rename from Application/EdFi.Ods.AdminApi/Infrastructure/OperationalContext.cs
rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/OperationalContext.cs
index de8412f09..881468569 100644
--- a/Application/EdFi.Ods.AdminApi/Infrastructure/OperationalContext.cs
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/OperationalContext.cs
@@ -3,9 +3,9 @@
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.
-namespace EdFi.Ods.AdminApi.Infrastructure;
+namespace EdFi.Ods.AdminApi.Common.Infrastructure;
-public class OperationalContext
+public static class OperationalContext
{
public const string DefaultOperationalContextUri = "uri://ed-fi-api-host.org";
}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Providers/Aes256SymmetricStringEncryptionProvider.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Providers/Aes256SymmetricStringEncryptionProvider.cs
new file mode 100644
index 000000000..07f84607d
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Providers/Aes256SymmetricStringEncryptionProvider.cs
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using System.Security.Cryptography;
+using EdFi.Ods.AdminApi.Common.Infrastructure.Providers.Interfaces;
+using log4net;
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Providers;
+
+///
+/// Implements AES 256 bit symmetric key encryption of string values.
+///
+///
+/// This class is used to facilitate the encryption of string values using the AES 256 bit
+/// encryption algorithm. HMAC signing of the encrypted value is used to mitigate potential
+/// timing-based padding oracle attacks on AES in CBC mode (as discussed here
+/// https://learn.microsoft.com/en-us/dotnet/standard/security/vulnerabilities-cbc-mode)
+/// by providing a means of verifying the authenticity of the encrypted content before decryption is attempted.
+public class Aes256SymmetricStringEncryptionProvider : ISymmetricStringEncryptionProvider
+{
+
+ private readonly ILog _logger;
+
+ public Aes256SymmetricStringEncryptionProvider()
+ {
+ _logger = LogManager.GetLogger(typeof(Aes256SymmetricStringEncryptionProvider));
+ }
+
+ ///
+ /// Encrypt the specified string value using the specified key.
+ ///
+ /// The string value to be encrypted.
+ /// The 256 bit private key to be used for encryption.
+ /// A string representing the input in encrypted form along with the other information
+ /// necessary to decrypt it (aside from the private key). The output is three strings concatenated in
+ /// the format "IV|EncryptedMessage|HMACSignature"
+ public string Encrypt(string? value, byte[]? key)
+ {
+ // Incorporates code from https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.aes?view=net-6.0
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ throw new ArgumentException("Input value cannot be null or whitespace.", nameof(value));
+ }
+
+ if (key == null || key.Length != 32)
+ {
+ throw new ArgumentException("Key must be 256 bits (32 bytes) in length.", nameof(key));
+ }
+
+ using Aes aesInstance = Aes.Create();
+
+ aesInstance.Key = key;
+ aesInstance.GenerateIV();
+ byte[] encryptedBytes;
+
+ ICryptoTransform encryptor = aesInstance.CreateEncryptor(aesInstance.Key, aesInstance.IV);
+
+ using (MemoryStream msEncrypt = new())
+ {
+ using CryptoStream csEncrypt = new(msEncrypt, encryptor, CryptoStreamMode.Write);
+ using (StreamWriter swEncrypt = new(csEncrypt))
+ {
+ swEncrypt.Write(value);
+ }
+
+ encryptedBytes = msEncrypt.ToArray();
+ }
+
+ byte[] hmacSignatureValue = GenerateHmacSignature();
+
+ return
+ $"{Convert.ToBase64String(aesInstance.IV)}|{Convert.ToBase64String(encryptedBytes)}|{Convert.ToBase64String(hmacSignatureValue)}";
+
+ byte[] GenerateHmacSignature()
+ {
+ byte[] hmacSignature;
+
+ using (HMACSHA256 hmac = new(key))
+ {
+ hmacSignature = hmac.ComputeHash(encryptedBytes);
+ }
+
+ return hmacSignature;
+ }
+ }
+
+ ///
+ /// Decrypt a string value using the specified key.
+ ///
+ /// The data to be decrypted and related information as three base64
+ /// encoded segments concatenated in the format "IV|EncryptedMessage|HMACSignature"
+ /// The 256 bit private key to be used for decryption.
+ /// If decryption is successful, then the plaintext output, otherwise null
+ /// A boolean value indicating if decryption was successful.
+ public bool TryDecrypt(string value, byte[] key, out string? output)
+ {
+ // Includes code from https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.aes?view=net-6.0
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ throw new ArgumentException("Input value cannot be null or whitespace.", nameof(value));
+ }
+
+ if (key == null || key.Length != 32)
+ {
+ throw new ArgumentException("Key must be 256 bits (32 bytes) in length.", nameof(key));
+ }
+
+ string[] inputParts = value.Split('|');
+
+ if (inputParts.Length != 3)
+ {
+ output = null;
+ return false;
+ }
+
+ string base64Iv = inputParts[0];
+ string base64EncryptedBytes = inputParts[1];
+ string base64HashValue = inputParts[2];
+ byte[] iv;
+ byte[] encryptedBytes;
+ byte[] hashValue;
+
+ try
+ {
+ iv = Convert.FromBase64String(base64Iv);
+ encryptedBytes = Convert.FromBase64String(base64EncryptedBytes);
+ hashValue = Convert.FromBase64String(base64HashValue);
+ }
+ catch (Exception e)
+ {
+ _logger.Error("Unable to convert encryption key from Base 64 String.", e);
+ throw new ArgumentException("Unable to convert key from Base 64 String.", nameof(key));
+ }
+
+ bool signatureIsValid = IsHmacSignatureValid();
+
+ if (!signatureIsValid)
+ {
+ output = null;
+ return false;
+ }
+
+ using Aes aesInstance = Aes.Create();
+
+ aesInstance.Key = key;
+ aesInstance.IV = iv;
+
+ ICryptoTransform decryptor = aesInstance.CreateDecryptor(aesInstance.Key, aesInstance.IV);
+
+ using MemoryStream msDecrypt = new(encryptedBytes);
+ using CryptoStream csDecrypt = new(msDecrypt, decryptor, CryptoStreamMode.Read);
+ using StreamReader srDecrypt = new(csDecrypt);
+ output = srDecrypt.ReadToEnd();
+
+ return true;
+
+ bool IsHmacSignatureValid()
+ {
+ bool signatureCheckResult;
+
+ using (HMACSHA256 hmac = new(key))
+ {
+ byte[] computedHashValue = hmac.ComputeHash(encryptedBytes);
+ signatureCheckResult = hashValue.SequenceEqual(computedHashValue);
+ }
+
+ return signatureCheckResult;
+ }
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Providers/Interfaces/ISymmetricStringEncryptionProvider.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Providers/Interfaces/ISymmetricStringEncryptionProvider.cs
new file mode 100644
index 000000000..ff9f3e326
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Providers/Interfaces/ISymmetricStringEncryptionProvider.cs
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Providers.Interfaces;
+
+///
+/// Defines an interface for providing symmetric encryption of string values.
+///
+///
+/// This interface and its implementations are used to facilitate the encryption and decryption
+/// of string values using a symmetric encryption algorithm.
+public interface ISymmetricStringEncryptionProvider
+{
+ ///
+ /// Encrypts the specified string value using the specified key.
+ ///
+ /// The string value to be encrypted.
+ /// The private key to be used for encryption.
+ /// A string representing the input in encrypted form along with any
+ /// other information necessary to decrypt it (aside from the private key)
+ public string Encrypt(string? value, byte[]? key);
+
+ ///
+ /// Decrypts the specified string value using the specified key.
+ ///
+ /// The string value to be decrypted.
+ /// The private key to be used for decryption.
+ /// TThe decrypted string value
+ /// Indicates if the decryption operation was successful
+ public bool TryDecrypt(string value, byte[] key, out string? output);
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Security/AuthorizationPolicies.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Security/AuthorizationPolicies.cs
new file mode 100644
index 000000000..0723f015d
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Security/AuthorizationPolicies.cs
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using System.Data;
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Security
+{
+ public static class AuthorizationPolicies
+ {
+ // Create policies by scope
+ public static readonly PolicyDefinition AdminApiFullAccessScopePolicy = new PolicyDefinition("AdminApiFullAccessScopePolicy", SecurityConstants.Scopes.AdminApiFullAccess.Scope);
+ public static readonly PolicyDefinition DefaultScopePolicy = AdminApiFullAccessScopePolicy;
+ public static readonly IEnumerable ScopePolicies = new List
+ {
+ AdminApiFullAccessScopePolicy,
+ };
+ }
+
+ public class PolicyDefinition
+ {
+ public string PolicyName { get; }
+ public string Scope { get; }
+
+ public PolicyDefinition(string policyName, string scope)
+ {
+ PolicyName = policyName;
+ Scope = scope;
+ }
+ public override string ToString()
+ {
+ return this.PolicyName;
+ }
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Security/SecurityConstants.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Security/SecurityConstants.cs
new file mode 100644
index 000000000..e06c50870
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Security/SecurityConstants.cs
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Security;
+
+public static class SecurityConstants
+{
+ public const string ConnectRoute = "connect";
+ public const string TokenActionName = "token";
+ public const string TokenEndpoint = $"{ConnectRoute}/{TokenActionName}";
+ public const string RegisterActionName = "register";
+ public const string DefaultRoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role";
+
+ public static class Scopes
+ {
+ public static readonly ScopeDefinition AdminApiFullAccess = new("edfi_admin_api/full_access", "Full access to the Admin API");
+
+ public static IEnumerable AllScopes =
+ [
+ AdminApiFullAccess
+ ];
+ }
+
+ public class ScopeDefinition(string scope, string scopeDescription)
+ {
+ public string Scope { get; } = scope;
+ public string ScopeDescription { get; } = scopeDescription;
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/OdsApiValidator.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Services/OdsApiValidator.cs
similarity index 97%
rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/OdsApiValidator.cs
rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/Services/OdsApiValidator.cs
index e74f567a8..cdaa99ad7 100644
--- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/OdsApiValidator.cs
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Services/OdsApiValidator.cs
@@ -4,13 +4,12 @@
// See the LICENSE and NOTICES files in the project root for more information.
using System.Net;
-using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling;
-using EdFi.Ods.AdminApi.Infrastructure.Services;
+using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NJsonSchema;
-namespace EdFi.Ods.AdminApi.Infrastructure.Api;
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Services;
public interface IOdsApiValidator
{
diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Services/SimpleGetRequest.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Services/SimpleGetRequest.cs
new file mode 100644
index 000000000..511483955
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Services/SimpleGetRequest.cs
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using System.Net;
+
+namespace EdFi.Ods.AdminApi.Common.Infrastructure.Services;
+
+public interface ISimpleGetRequest
+{
+ public Task DownloadString(string address);
+}
+
+public class SimpleGetRequest : ISimpleGetRequest
+{
+ private readonly IHttpClientFactory _clientFactory;
+
+ public SimpleGetRequest(IHttpClientFactory clientFactory)
+ {
+ _clientFactory = clientFactory;
+ }
+
+ public async Task DownloadString(string address)
+ {
+ var httpClient = _clientFactory.CreateClient();
+
+ using (var response = await httpClient.GetAsync(address))
+ {
+ await CheckResponseStatusCode(address, response);
+
+ using (var content = response.Content)
+ {
+ return await content.ReadAsStringAsync();
+ }
+ }
+
+ static async Task CheckResponseStatusCode(string requestUrl, HttpResponseMessage response)
+ {
+ switch (response.StatusCode)
+ {
+ case HttpStatusCode.OK:
+ break;
+ case 0:
+ throw new HttpRequestException($"No response from {requestUrl}.");
+ case HttpStatusCode.NotFound:
+ throw new HttpRequestException($"{requestUrl} not found.", null, HttpStatusCode.NotFound);
+ case HttpStatusCode.ServiceUnavailable:
+ throw new HttpRequestException(
+ $"{requestUrl} is unavailable", null, HttpStatusCode.ServiceUnavailable);
+ case HttpStatusCode.BadGateway:
+ throw new HttpRequestException(
+ $"{requestUrl} was acting as a gateway or proxy and received an invalid response from the upstream server.",
+ null, HttpStatusCode.BadGateway);
+ case (HttpStatusCode)495:
+ throw new HttpRequestException(
+ $"Invalid SSL client certificate for {requestUrl}.", null, (HttpStatusCode)495);
+ case (HttpStatusCode)496:
+ throw new HttpRequestException(
+ $"Missing SSL client certificate for {requestUrl}.", null, (HttpStatusCode)496);
+ case HttpStatusCode.BadRequest:
+ throw new InvalidOperationException($"Malformed request for {requestUrl}.");
+ default:
+ var message = $"Unexpected response from {requestUrl}";
+
+ using (var content = response.Content)
+ {
+ var details = await content.ReadAsStringAsync();
+
+ if (!string.IsNullOrEmpty(details))
+ message += $": {details}";
+ }
+
+ throw new HttpRequestException(message, null, HttpStatusCode.ServiceUnavailable);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/ValidatorExtensions.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ValidatorExtensions.cs
similarity index 92%
rename from Application/EdFi.Ods.AdminApi/Infrastructure/ValidatorExtensions.cs
rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/ValidatorExtensions.cs
index f2600ee2d..265bfa9c8 100644
--- a/Application/EdFi.Ods.AdminApi/Infrastructure/ValidatorExtensions.cs
+++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ValidatorExtensions.cs
@@ -5,6 +5,8 @@
using FluentValidation;
+namespace EdFi.Ods.AdminApi.Common.Infrastructure;
+
public static class ValidatorExtensions
{
public static async Task GuardAsync(this IValidator validator, TRequest request)
diff --git a/Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs
new file mode 100644
index 000000000..635140d3c
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+namespace EdFi.Ods.AdminApi.Common.Settings;
+
+public class AppSettingsFile
+{
+ public required AppSettings AppSettings { get; set; }
+ public required SwaggerSettings SwaggerSettings { get; set; }
+ public Dictionary ConnectionStrings { get; set; } = [];
+ public Dictionary Tenants { get; set; } = new(StringComparer.OrdinalIgnoreCase);
+ public required TestingSettings Testing { get; set; }
+}
+
+public class AppSettings
+{
+ public int DefaultPageSizeOffset { get; set; }
+ public int DefaultPageSizeLimit { get; set; }
+ public string? DatabaseEngine { get; set; }
+ public string? EncryptionKey { get; set; }
+ public bool MultiTenancy { get; set; }
+ public bool PreventDuplicateApplications { get; set; }
+ public bool EnableApplicationResetEndpoint { get; set; }
+ public string? AdminApiMode { get; set; }
+}
+
+public class SwaggerSettings
+{
+ public bool EnableSwagger { get; set; }
+ public string? DefaultTenant { get; set; }
+}
+
+
+public class IpRateLimitingOptions
+{
+ public bool EnableEndpointRateLimiting { get; set; }
+ public bool StackBlockedRequests { get; set; }
+ public string RealIpHeader { get; set; } = string.Empty;
+ public string ClientIdHeader { get; set; } = string.Empty;
+ public int HttpStatusCode { get; set; }
+ public List? IpWhitelist { get; set; }
+ public List? EndpointWhitelist { get; set; }
+ public List? GeneralRules { get; set; }
+
+ public class GeneralRule
+ {
+ public string Endpoint { get; set; } = string.Empty;
+ public string Period { get; set; } = string.Empty;
+ public int Limit { get; set; }
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Settings/CorsSettings.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/CorsSettings.cs
new file mode 100644
index 000000000..c3c247e70
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Settings/CorsSettings.cs
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+namespace EdFi.Ods.AdminApi.Common.Settings;
+
+public class CorsSettings
+{
+ public bool EnableCors { get; set; }
+ public string[] AllowedOrigins { get; set; } = [];
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Settings/FileHelpers.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/FileHelpers.cs
new file mode 100644
index 000000000..484b70da6
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Settings/FileHelpers.cs
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+namespace EdFi.Ods.AdminApi.Common.Settings;
+
+public static class FileHelpers
+{
+ public static byte[] ComputeHash(string filePath)
+ {
+ var runCount = 1;
+
+ while (runCount < 4)
+ {
+ try
+ {
+ if (File.Exists(filePath))
+ {
+ using var fs = File.OpenRead(filePath);
+ return System.Security.Cryptography.SHA1
+ .Create().ComputeHash(fs);
+ }
+ else
+ {
+ throw new FileNotFoundException();
+ }
+ }
+ catch (IOException)
+ {
+ if (runCount == 3)
+ {
+ throw;
+ }
+
+ Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
+ runCount++;
+ }
+ }
+
+ return new byte[20];
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi/Helpers/AppSettings.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/IEncryptionKeySettings.cs
similarity index 62%
rename from Application/EdFi.Ods.AdminApi/Helpers/AppSettings.cs
rename to Application/EdFi.Ods.AdminApi.Common/Settings/IEncryptionKeySettings.cs
index 58b10f1b3..3c111419b 100644
--- a/Application/EdFi.Ods.AdminApi/Helpers/AppSettings.cs
+++ b/Application/EdFi.Ods.AdminApi.Common/Settings/IEncryptionKeySettings.cs
@@ -3,9 +3,9 @@
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.
-namespace EdFi.Ods.AdminApi.Helpers;
-public class AppSettings
+namespace EdFi.Ods.AdminApi.Common.Settings;
+
+public interface IEncryptionKeySettings
{
- public int DefaultPageSizeOffset { get; set; }
- public int DefaultPageSizeLimit { get; set; }
+ public string EncryptionKey { get; set; }
}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Settings/TenantSettings.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/TenantSettings.cs
new file mode 100644
index 000000000..284a4758a
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Settings/TenantSettings.cs
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+namespace EdFi.Ods.AdminApi.Common.Settings;
+
+public class TenantsSection
+{
+ public Dictionary Tenants { get; set; } = new(StringComparer.OrdinalIgnoreCase);
+}
+
+public class TenantSettings
+{
+ public Dictionary ConnectionStrings { get; set; } = new(StringComparer.OrdinalIgnoreCase);
+}
diff --git a/Application/EdFi.Ods.AdminApi.Common/Settings/TestingSettings.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/TestingSettings.cs
new file mode 100644
index 000000000..90b239a4d
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.Common/Settings/TestingSettings.cs
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling;
+
+namespace EdFi.Ods.AdminApi.Common.Settings;
+
+public class TestingSettings
+{
+ public bool InjectException { get; set; }
+
+ public void CheckIfHasToThrowException()
+ {
+ if (InjectException)
+ throw new AdminApiException("Exception to test");
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/AddClaimSetCommandV6ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/AddClaimSetCommandTests.cs
similarity index 74%
rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/AddClaimSetCommandV6ServiceTests.cs
rename to Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/AddClaimSetCommandTests.cs
index e7438f219..a94ee4d77 100644
--- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/AddClaimSetCommandV6ServiceTests.cs
+++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/AddClaimSetCommandTests.cs
@@ -3,35 +3,27 @@
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.
-using System;
-using System.Linq;
-using NUnit.Framework;
using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor;
+using NUnit.Framework;
using Shouldly;
-using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet;
-using Application = EdFi.Security.DataAccess.Models.Application;
+using System.Linq;
+using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet;
namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests;
[TestFixture]
-public class AddClaimSetCommandV6ServiceTests : SecurityDataTestBase
+public class AddClaimSetCommandTests : SecurityDataTestBase
{
[Test]
public void ShouldAddClaimSet()
{
- var testApplication = new Application
- {
- ApplicationName = $"Test Application {DateTime.Now:O}"
- };
- Save(testApplication);
-
var newClaimSet = new AddClaimSetModel { ClaimSetName = "TestClaimSet" };
var addedClaimSetId = 0;
ClaimSet addedClaimSet = null;
using (var securityContext = TestContext)
{
- var command = new AddClaimSetCommandV6Service(securityContext);
+ var command = new AddClaimSetCommand(securityContext);
addedClaimSetId = command.Execute(newClaimSet);
addedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == addedClaimSetId);
}
diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/CopyClaimSetCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/CopyClaimSetCommandTests.cs
new file mode 100644
index 000000000..5f3b66d80
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/CopyClaimSetCommandTests.cs
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using System.Linq;
+using EdFi.Ods.AdminApp.Management.ClaimSetEditor;
+using Moq;
+using NUnit.Framework;
+using Shouldly;
+using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet;
+
+namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests;
+
+[TestFixture]
+public class CopyClaimSetCommandTests : SecurityDataTestBase
+{
+ [Test]
+ public void ShouldCopyClaimSet()
+ {
+ var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" };
+ Save(testClaimSet);
+
+ var testResourceClaims = SetupClaimSetResourceClaimActions(testClaimSet,
+ UniqueNameList("ParentRc", 3), UniqueNameList("ChildRc", 1));
+
+ var newClaimSet = new Mock();
+ newClaimSet.Setup(x => x.Name).Returns("TestClaimSet_Copy");
+ newClaimSet.Setup(x => x.OriginalId).Returns(testClaimSet.ClaimSetId);
+
+ var copyClaimSetId = 0;
+ ClaimSet copiedClaimSet = null;
+ using var securityContext = TestContext;
+ var command = new CopyClaimSetCommand(securityContext);
+ copyClaimSetId = command.Execute(newClaimSet.Object);
+ copiedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == copyClaimSetId);
+
+ copiedClaimSet.ClaimSetName.ShouldBe(newClaimSet.Object.Name);
+ copiedClaimSet.ForApplicationUseOnly.ShouldBe(false);
+ copiedClaimSet.IsEdfiPreset.ShouldBe(false);
+
+ var results = ResourceClaimsForClaimSet(copiedClaimSet.ClaimSetId).ToList();
+
+ var testParentResourceClaimsForId =
+ testResourceClaims.Where(x => x.ClaimSet.ClaimSetId == testClaimSet.ClaimSetId && x.ResourceClaim.ParentResourceClaim == null).Select(x => x.ResourceClaim).ToArray();
+
+ results.Count.ShouldBe(testParentResourceClaimsForId.Length);
+ results.Select(x => x.Name).ShouldBe(testParentResourceClaimsForId.Select(x => x.ResourceName), true);
+ results.Select(x => x.Id).ShouldBe(testParentResourceClaimsForId.Select(x => x.ResourceClaimId), true);
+ results.All(x => x.Actions.All(x => x.Name.Equals("Create") && x.Enabled)).ShouldBe(true);
+
+ foreach (var testParentResourceClaim in testParentResourceClaimsForId)
+ {
+ var testChildren = securityContext.ResourceClaims.Where(x =>
+ x.ParentResourceClaimId == testParentResourceClaim.ResourceClaimId).ToList();
+ var parentResult = results.First(x => x.Id == testParentResourceClaim.ResourceClaimId);
+ parentResult.Children.Select(x => x.Name).ShouldBe(testChildren.Select(x => x.ResourceName), true);
+ parentResult.Children.Select(x => x.Id).ShouldBe(testChildren.Select(x => x.ResourceClaimId), true);
+ parentResult.Children.All(x => x.Actions.All(x => x.Name.Equals("Create") && x.Enabled)).ShouldBe(true);
+ }
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandTests.cs
similarity index 69%
rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandV53ServiceTests.cs
rename to Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandTests.cs
index b418a9bd6..fda9c96a7 100644
--- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandV53ServiceTests.cs
+++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandTests.cs
@@ -2,54 +2,45 @@
// Licensed to the Ed-Fi Alliance under one or more agreements.
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.
-extern alias Compatability;
-using System;
-using System.Linq;
-using NUnit.Framework;
using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor;
-using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling;
+using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling;
using Moq;
+using NUnit.Framework;
using Shouldly;
-using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet;
-using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application;
+using System.Linq;
+using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet;
namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests;
[TestFixture]
-public class DeleteClaimSetCommandV53ServiceTests : SecurityData53TestBase
+public class DeleteClaimSetCommandTests : SecurityDataTestBase
{
[Test]
public void ShouldDeleteClaimSet()
{
- var testApplication = new Application
- {
- ApplicationName = $"Test Application {DateTime.Now:O}"
- };
- Save(testApplication);
-
var testClaimSetToDelete = new ClaimSet
- { ClaimSetName = "TestClaimSet_Delete", Application = testApplication };
+ { ClaimSetName = "TestClaimSet_Delete" };
Save(testClaimSetToDelete);
- SetupParentResourceClaimsWithChildren(testClaimSetToDelete, testApplication);
+ SetupClaimSetResourceClaimActions(testClaimSetToDelete, UniqueNameList("ParentRc", 3), UniqueNameList("ChildRc", 1));
var testClaimSetToPreserve = new ClaimSet
- { ClaimSetName = "TestClaimSet_Preserve", Application = testApplication };
+ { ClaimSetName = "TestClaimSet_Preserve" };
Save(testClaimSetToPreserve);
- var resourceClaimsForPreservedClaimSet = SetupParentResourceClaimsWithChildren(testClaimSetToPreserve, testApplication);
+ var resourceClaimsForPreservedClaimSet = SetupClaimSetResourceClaimActions(testClaimSetToPreserve, UniqueNameList("ParentRc", 3),
+ UniqueNameList("ChildRc", 1));
var deleteModel = new Mock();
deleteModel.Setup(x => x.Name).Returns(testClaimSetToDelete.ClaimSetName);
deleteModel.Setup(x => x.Id).Returns(testClaimSetToDelete.ClaimSetId);
using var securityContext = TestContext;
- var command = new DeleteClaimSetCommandV53Service(securityContext);
+ var command = new DeleteClaimSetCommand(securityContext);
command.Execute(deleteModel.Object);
-
var deletedClaimSet = securityContext.ClaimSets.SingleOrDefault(x => x.ClaimSetId == testClaimSetToDelete.ClaimSetId);
deletedClaimSet.ShouldBeNull();
- var deletedClaimSetResourceActions = securityContext.ClaimSetResourceClaims.Count(x => x.ClaimSet.ClaimSetId == testClaimSetToDelete.ClaimSetId);
+ var deletedClaimSetResourceActions = securityContext.ClaimSetResourceClaimActions.Count(x => x.ClaimSet.ClaimSetId == testClaimSetToDelete.ClaimSetId);
deletedClaimSetResourceActions.ShouldBe(0);
var preservedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == testClaimSetToPreserve.ClaimSetId);
@@ -63,7 +54,7 @@ public void ShouldDeleteClaimSet()
results.Count.ShouldBe(testParentResourceClaimsForId.Length);
results.Select(x => x.Name).ShouldBe(testParentResourceClaimsForId.Select(x => x.ResourceName), true);
results.Select(x => x.Id).ShouldBe(testParentResourceClaimsForId.Select(x => x.ResourceClaimId), true);
- results.All(x => x.Create).ShouldBe(true);
+ results.All(x => x.Actions != null && x.Actions.Any(x => x.Name.Equals("Create") && x.Enabled)).ShouldBe(true);
foreach (var testParentResourceClaim in testParentResourceClaimsForId)
{
@@ -72,20 +63,14 @@ public void ShouldDeleteClaimSet()
var parentResult = results.First(x => x.Id == testParentResourceClaim.ResourceClaimId);
parentResult.Children.Select(x => x.Name).ShouldBe(testChildren.Select(x => x.ResourceName), true);
parentResult.Children.Select(x => x.Id).ShouldBe(testChildren.Select(x => x.ResourceClaimId), true);
- parentResult.Children.All(x => x.Create).ShouldBe(true);
+ parentResult.Children.All(x => x.Actions != null && x.Actions.Any(x => x.Name.Equals("Create") && x.Enabled)).ShouldBe(true);
}
}
[Test]
public void ShouldThrowExceptionOnEditSystemReservedClaimSet()
{
- var testApplication = new Application
- {
- ApplicationName = $"Test Application {DateTime.Now:O}"
- };
- Save(testApplication);
-
- var systemReservedClaimSet = new ClaimSet { ClaimSetName = "SIS Vendor", Application = testApplication };
+ var systemReservedClaimSet = new ClaimSet { ClaimSetName = "SIS Vendor", IsEdfiPreset = true };
Save(systemReservedClaimSet);
var deleteModel = new Mock();
@@ -94,10 +79,10 @@ public void ShouldThrowExceptionOnEditSystemReservedClaimSet()
using var securityContext = TestContext;
var exception = Assert.Throws(() =>
{
- var command = new DeleteClaimSetCommandV53Service(securityContext);
+ var command = new DeleteClaimSetCommand(securityContext);
command.Execute(deleteModel.Object);
});
exception.ShouldNotBeNull();
- exception.Message.ShouldBe($"Claim set({systemReservedClaimSet.ClaimSetName}) is system reserved.Can not be deleted.");
+ exception.Message.ShouldBe($"Claim set({systemReservedClaimSet.ClaimSetName}) is system reserved. Can not be deleted.");
}
}
diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteResourceClaimOnClaimSetCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteResourceClaimOnClaimSetCommandTests.cs
new file mode 100644
index 000000000..5185ded30
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteResourceClaimOnClaimSetCommandTests.cs
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor;
+using NUnit.Framework;
+using Shouldly;
+using System.Linq;
+using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet;
+
+namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests;
+
+[TestFixture]
+public class DeleteResourceClaimOnClaimSetCommandTests : SecurityDataTestBase
+{
+ [Test]
+ public void ShouldDeleteResourceClaimOnClaimSet()
+ {
+ var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" };
+ Save(testClaimSet);
+
+ var parentRcNames = UniqueNameList("ParentRc", 2);
+ var testResources = SetupClaimSetResourceClaimActions(testClaimSet, parentRcNames, UniqueNameList("ChildRc", 1));
+
+ using var securityContext = TestContext;
+ var command = new DeleteResouceClaimOnClaimSetCommand(securityContext);
+ command.Execute(testClaimSet.ClaimSetId, testResources.First().ResourceClaimId);
+
+ var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId);
+
+ resourceClaimsForClaimSet.Count.ShouldBeLessThan(testResources.Count);
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditClaimSetCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditClaimSetCommandTests.cs
new file mode 100644
index 000000000..86f6a3bf4
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditClaimSetCommandTests.cs
@@ -0,0 +1,193 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using EdFi.Ods.AdminApi.Features.ClaimSets;
+using EdFi.Ods.AdminApi.Infrastructure;
+using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor;
+using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling;
+using EdFi.Security.DataAccess.Contexts;
+using NUnit.Framework;
+using Shouldly;
+using System;
+using System.Linq;
+using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet;
+using VendorApplication = EdFi.Admin.DataAccess.Models.Application;
+using EdFi.Ods.AdminApi.Common.Infrastructure;
+
+namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests;
+
+[TestFixture]
+public class EditClaimSetCommandTests : SecurityDataTestBase
+{
+ [Test]
+ public void ShouldEditClaimSet()
+ {
+ var alreadyExistingClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" };
+ Save(alreadyExistingClaimSet);
+
+ var editModel = new EditClaimSetModel { ClaimSetName = "TestClaimSetEdited", ClaimSetId = alreadyExistingClaimSet.ClaimSetId };
+
+ using var securityContext = TestContext;
+ UsersTransaction((usersContext) =>
+ {
+ var command = new EditClaimSetCommand(securityContext, usersContext);
+ command.Execute(editModel);
+ });
+
+ var editedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == alreadyExistingClaimSet.ClaimSetId);
+ editedClaimSet.ClaimSetName.ShouldBe(editModel.ClaimSetName);
+ }
+
+ [Test]
+ public void ShouldThrowExceptionOnEditSystemReservedClaimSet()
+ {
+ var systemReservedClaimSet = new ClaimSet { ClaimSetName = "Ed-Fi Sandbox", IsEdfiPreset = true };
+ Save(systemReservedClaimSet);
+
+ var editModel = new EditClaimSetModel { ClaimSetName = "TestClaimSetEdited", ClaimSetId = systemReservedClaimSet.ClaimSetId };
+
+ using var securityContext = TestContext;
+ var exception = Assert.Throws(() => UsersTransaction(usersContext =>
+ {
+ var command = new EditClaimSetCommand(TestContext, usersContext);
+ command.Execute(editModel);
+ }));
+ exception.ShouldNotBeNull();
+ exception.Message.ShouldBe($"Claim set ({systemReservedClaimSet.ClaimSetName}) is system reserved. May not be modified.");
+ }
+
+ [Test]
+ public void ShouldEditClaimSetWithVendorApplications()
+ {
+ var claimSetToBeEdited = new ClaimSet { ClaimSetName = $"TestClaimSet{Guid.NewGuid():N}" };
+ Save(claimSetToBeEdited);
+ SetupVendorApplicationsForClaimSet(claimSetToBeEdited);
+
+ var claimSetNotToBeEdited = new ClaimSet { ClaimSetName = $"TestClaimSet{Guid.NewGuid():N}" };
+ Save(claimSetNotToBeEdited);
+ SetupVendorApplicationsForClaimSet(claimSetNotToBeEdited);
+
+ var editModel = new EditClaimSetModel { ClaimSetName = "TestClaimSetEdited", ClaimSetId = claimSetToBeEdited.ClaimSetId };
+
+ using var securityContext = TestContext;
+ UsersTransaction(usersContext =>
+ {
+ var command = new EditClaimSetCommand(securityContext, usersContext);
+ command.Execute(editModel);
+ });
+
+ var editedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == claimSetToBeEdited.ClaimSetId);
+ editedClaimSet.ClaimSetName.ShouldBe(editModel.ClaimSetName);
+ AssertApplicationsForClaimSet(claimSetToBeEdited.ClaimSetId, editModel.ClaimSetName, securityContext);
+
+ var unEditedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == claimSetNotToBeEdited.ClaimSetId);
+ unEditedClaimSet.ClaimSetName.ShouldBe(claimSetNotToBeEdited.ClaimSetName);
+ AssertApplicationsForClaimSet(claimSetNotToBeEdited.ClaimSetId, claimSetNotToBeEdited.ClaimSetName, securityContext);
+ }
+
+ private void SetupVendorApplicationsForClaimSet(ClaimSet testClaimSet, int applicationCount = 5)
+ {
+ UsersTransaction(usersContext =>
+ {
+ foreach (var _ in Enumerable.Range(1, applicationCount))
+ {
+ usersContext.Applications.Add(new VendorApplication
+ {
+ ApplicationName = $"TestAppVendorName{Guid.NewGuid():N}",
+ ClaimSetName = testClaimSet.ClaimSetName,
+ OperationalContextUri = OperationalContext.DefaultOperationalContextUri
+ });
+ }
+ usersContext.SaveChanges();
+ });
+ }
+
+ private void AssertApplicationsForClaimSet(int claimSetId, string claimSetNameToAssert, ISecurityContext securityContext)
+ {
+ UsersTransaction(
+ usersContext =>
+ {
+ var results = new GetApplicationsByClaimSetIdQuery(securityContext, usersContext).Execute(claimSetId);
+ var testApplications = usersContext.Applications.Where(x => x.ClaimSetName == claimSetNameToAssert).ToList();
+ results.Count().ShouldBe(testApplications.Count);
+ results.Select(x => x.Name).ShouldBe(testApplications.Select(x => x.ApplicationName), true);
+ });
+ }
+
+ // TODO: move these to UnitTests, using appropriate validator from the API project
+ //[Test]
+ //public void ShouldNotEditClaimSetIfNameNotUnique()
+ //{
+ // var testApplication = new Application
+ // {
+ // ApplicationName = $"Test Application {DateTime.Now:O}"
+ // };
+ // SaveAdminContext(testApplication);
+
+ // var alreadyExistingClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet1", Application = testApplication };
+ // Save(alreadyExistingClaimSet);
+
+ // var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet2", Application = testApplication };
+ // Save(testClaimSet);
+
+ // var editModel = new EditClaimSetModel { ClaimSetName = "TestClaimSet1", ClaimSetId = testClaimSet.ClaimSetId };
+
+ // using var securityContext = TestContext;
+ // var validator = new EditClaimSetModelValidator(AllClaimSetsQuery(securityContext),
+ // ClaimSetByIdQuery(securityContext));
+ // var validationResults = validator.Validate(editModel);
+ // validationResults.IsValid.ShouldBe(false);
+ // validationResults.Errors.Single().ErrorMessage.ShouldBe("A claim set with this name already exists in the database. Please enter a unique name.");
+ //}
+
+ //[Test]
+ //public void ShouldNotEditClaimSetIfNameEmpty()
+ //{
+ // var testApplication = new Application
+ // {
+ // ApplicationName = $"Test Application {DateTime.Now:O}"
+ // };
+ // SaveAdminContext(testApplication);
+
+ // var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet1", Application = testApplication };
+ // Save(testClaimSet);
+
+ // var editModel = new EditClaimSetModel { ClaimSetName = "", ClaimSetId = testClaimSet.ClaimSetId };
+
+ // using var securityContext = TestContext;
+ // var validator = new EditClaimSetModelValidator(AllClaimSetsQuery(securityContext),
+ // ClaimSetByIdQuery(securityContext));
+ // var validationResults = validator.Validate(editModel);
+ // validationResults.IsValid.ShouldBe(false);
+ // validationResults.Errors.Single().ErrorMessage.ShouldBe("'Claim Set Name' must not be empty.");
+ //}
+
+ //[Test]
+ //public void ShouldNotEditClaimSetIfNameLengthGreaterThan255Characters()
+ //{
+ // var testApplication = new Application
+ // {
+ // ApplicationName = $"Test Application {DateTime.Now:O}"
+ // };
+ // SaveAdminContext(testApplication);
+
+ // var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet1", Application = testApplication };
+ // Save(testClaimSet);
+
+ // var editModel = new EditClaimSetModel { ClaimSetName = "ThisIsAClaimSetWithNameLengthGreaterThan255CharactersThisIsAClaimSetWithNameLengthGreaterThan255CharactersThisIsAClaimSetWithNameLengthGreaterThan255CharactersThisIsAClaimSetWithNameLengthGreaterThan255CharactersThisIsAClaimSetWithNameLengthGreaterThan255CharactersThisIsAClaimSetWithNameLengthGreaterThan255Characters", ClaimSetId = testClaimSet.ClaimSetId };
+
+ // using var securityContext = TestContext;
+ // var validator = new EditClaimSetModelValidator(AllClaimSetsQuery(securityContext),
+ // ClaimSetByIdQuery(securityContext));
+ // var validationResults = validator.Validate(editModel);
+ // validationResults.IsValid.ShouldBe(false);
+ // validationResults.Errors.Single().ErrorMessage.ShouldBe("The claim set name must be less than 255 characters.");
+ //}
+
+ //private GetClaimSetByIdQuery ClaimSetByIdQuery(ISecurityContext securityContext) => new GetClaimSetByIdQuery(new StubOdsSecurityModelVersionResolver.V6(),
+ // null, new GetClaimSetByIdQueryV6Service(securityContext));
+
+ //private IGetAllClaimSetsQuery AllClaimSetsQuery(ISecurityContext securityContext) => new GetAllClaimSetsQuery(securityContext);
+}
diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditClaimSetCommandV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditClaimSetCommandV53ServiceTests.cs
deleted file mode 100644
index 8965229e5..000000000
--- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditClaimSetCommandV53ServiceTests.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// Licensed to the Ed-Fi Alliance under one or more agreements.
-// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
-// See the LICENSE and NOTICES files in the project root for more information.
-extern alias Compatability;
-
-using System;
-using System.Linq;
-using NUnit.Framework;
-using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor;
-using Shouldly;
-using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts;
-using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling;
-using VendorApplication = EdFi.Admin.DataAccess.Models.Application;
-using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet;
-using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application;
-using EdFi.Ods.AdminApi.Features.ClaimSets;
-using EdFi.Ods.AdminApi.Infrastructure;
-
-namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests;
-
-[TestFixture]
-public class EditClaimSetCommandV53ServiceTests : SecurityData53TestBase
-{
- [Test]
- public void ShouldEditClaimSet()
- {
- var testApplication = new Application
- {
- ApplicationName = $"Test Application {DateTime.Now:O}"
- };
- Save(testApplication);
-
- var alreadyExistingClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", Application = testApplication };
- Save(alreadyExistingClaimSet);
-
- var editModel = new EditClaimSetModel { ClaimSetName = "TestClaimSetEdited", ClaimSetId = alreadyExistingClaimSet.ClaimSetId };
-
- using var securityContext = TestContext;
- UsersTransaction((usersContext) =>
- {
- var command = new EditClaimSetCommandV53Service(securityContext, usersContext);
- command.Execute(editModel);
- });
-
- var editedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == alreadyExistingClaimSet.ClaimSetId);
- editedClaimSet.ClaimSetName.ShouldBe(editModel.ClaimSetName);
- }
-
- [Test]
- public void ShouldThrowExceptionOnEditSystemReservedClaimSet()
- {
- var testApplication = new Application
- {
- ApplicationName = $"Test Application {DateTime.Now:O}"
- };
- Save(testApplication);
-
- var systemReservedClaimSet = new ClaimSet { ClaimSetName = "Ed-Fi Sandbox", Application = testApplication };
- Save(systemReservedClaimSet);
-
- var editModel = new EditClaimSetModel { ClaimSetName = "TestClaimSetEdited", ClaimSetId = systemReservedClaimSet.ClaimSetId };
-
- using var securityContext = TestContext;
- var exception = Assert.Throws(() => UsersTransaction(usersContext =>
- {
- var command = new EditClaimSetCommandV53Service(securityContext, usersContext);
- command.Execute(editModel);
- }));
- exception.ShouldNotBeNull();
- exception.Message.ShouldBe($"Claim set ({systemReservedClaimSet.ClaimSetName}) is system reserved.May not be modified.");
- }
-
- [Test]
- public void ShouldEditClaimSetWithVendorApplications()
- {
- var testApplication = new Application
- {
- ApplicationName = $"Test Application {DateTime.Now:O}"
- };
- Save(testApplication);
-
- var claimSetToBeEdited = new ClaimSet { ClaimSetName = $"TestClaimSet{Guid.NewGuid():N}", Application = testApplication };
- Save(claimSetToBeEdited);
- SetupVendorApplicationsForClaimSet(claimSetToBeEdited);
-
- var claimSetNotToBeEdited = new ClaimSet { ClaimSetName = $"TestClaimSet{Guid.NewGuid():N}", Application = testApplication };
- Save(claimSetNotToBeEdited);
- SetupVendorApplicationsForClaimSet(claimSetNotToBeEdited);
-
- var editModel = new EditClaimSetModel { ClaimSetName = "TestClaimSetEdited", ClaimSetId = claimSetToBeEdited.ClaimSetId };
-
- using var securityContext = TestContext;
- UsersTransaction(usersContext =>
- {
- var command = new EditClaimSetCommandV53Service(securityContext, usersContext);
- command.Execute(editModel);
- });
-
- var editedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == claimSetToBeEdited.ClaimSetId);
- editedClaimSet.ClaimSetName.ShouldBe(editModel.ClaimSetName);
- AssertApplicationsForClaimSet(claimSetToBeEdited.ClaimSetId, editModel.ClaimSetName, securityContext);
-
-
- var unEditedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == claimSetNotToBeEdited.ClaimSetId);
- unEditedClaimSet.ClaimSetName.ShouldBe(claimSetNotToBeEdited.ClaimSetName);
- AssertApplicationsForClaimSet(claimSetNotToBeEdited.ClaimSetId, claimSetNotToBeEdited.ClaimSetName, securityContext);
- }
-
- private void SetupVendorApplicationsForClaimSet(ClaimSet testClaimSet, int applicationCount = 5)
- {
- UsersTransaction(usersContext =>
- {
- foreach (var _ in Enumerable.Range(1, applicationCount))
- {
- usersContext.Applications.Add(new VendorApplication
- {
- ApplicationName = $"TestAppVendorName{Guid.NewGuid():N}",
- ClaimSetName = testClaimSet.ClaimSetName,
- OperationalContextUri = OperationalContext.DefaultOperationalContextUri
- });
- }
- usersContext.SaveChanges();
- });
- }
-
- private void AssertApplicationsForClaimSet(int claimSetId, string claimSetNameToAssert, ISecurityContext securityContext)
- {
- UsersTransaction(
- usersContext =>
- {
- var results = new GetApplicationsByClaimSetId53Query(securityContext, usersContext).Execute(claimSetId);
- var testApplications = usersContext.Applications.Where(x => x.ClaimSetName == claimSetNameToAssert).ToList();
- results.Count().ShouldBe(testApplications.Count);
- results.Select(x => x.Name).ShouldBe(testApplications.Select(x => x.ApplicationName), true);
- });
- }
-}
diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandTests.cs
new file mode 100644
index 000000000..2597b1620
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandTests.cs
@@ -0,0 +1,295 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using System.Collections.Generic;
+using System.Linq;
+using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor;
+using Moq;
+using NUnit.Framework;
+using Shouldly;
+using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet;
+using ResourceClaim = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ResourceClaim;
+
+namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests;
+
+[TestFixture]
+public class EditResourceOnClaimSetCommandTests : SecurityDataTestBase
+{
+ [Test]
+ public void ShouldEditParentResourcesOnClaimSet()
+ {
+ var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" };
+ Save(testClaimSet);
+
+ var parentRcNames = UniqueNameList("ParentRc", 2);
+ var testResources = SetupClaimSetResourceClaimActions(testClaimSet, parentRcNames, UniqueNameList("ChildRc", 1));
+
+ var testResource1ToEdit = testResources.Select(x => x.ResourceClaim).Single(x => x.ResourceName == parentRcNames.First());
+ var testResource2ToNotEdit = testResources.Select(x => x.ResourceClaim).Single(x => x.ResourceName == parentRcNames.Last());
+
+ var editedResource = new ResourceClaim
+ {
+ Id = testResource1ToEdit.ResourceClaimId,
+ Name = testResource1ToEdit.ResourceName,
+ Actions = new List
+ {
+ new ResourceClaimAction{ Name = "Create", Enabled = false },
+ new ResourceClaimAction{ Name = "Read", Enabled = false },
+ new ResourceClaimAction{ Name = "Update", Enabled = true },
+ new ResourceClaimAction{ Name = "Delete", Enabled = true }
+ }
+ };
+
+ var editResourceOnClaimSetModel = new Mock();
+ editResourceOnClaimSetModel.Setup(x => x.ClaimSetId).Returns(testClaimSet.ClaimSetId);
+ editResourceOnClaimSetModel.Setup(x => x.ResourceClaim).Returns(editedResource);
+
+ using var securityContext = TestContext;
+ var command = new EditResourceOnClaimSetCommand(securityContext);
+ command.Execute(editResourceOnClaimSetModel.Object);
+
+ var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId);
+
+ var resultResourceClaim1 = resourceClaimsForClaimSet.Single(x => x.Id == editedResource.Id);
+
+ resultResourceClaim1.Actions.ShouldNotBeNull();
+ resultResourceClaim1.Actions.Count.ShouldBe(2);
+ resultResourceClaim1.Actions.All(x =>
+ editedResource.Actions.Any(y => x.Name.Equals(y.Name) && x.Enabled.Equals(y.Enabled))).ShouldBe(true);
+
+ var resultResourceClaim2 = resourceClaimsForClaimSet.Single(x => x.Id == testResource2ToNotEdit.ResourceClaimId);
+ resultResourceClaim2.Actions.ShouldNotBeNull();
+ resultResourceClaim2.Actions.Count.ShouldBe(1);
+ resultResourceClaim2.Actions.Any(x => x.Name.Equals("Create")).ShouldBe(true);
+ }
+
+ [Test]
+ public void ShouldEditChildResourcesOnClaimSet()
+ {
+ var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" };
+ Save(testClaimSet);
+
+ var parentRcNames = UniqueNameList("ParentRc", 1);
+ var childRcNames = UniqueNameList("ChildRc", 2);
+ var testResources = SetupClaimSetResourceClaimActions(testClaimSet, parentRcNames, childRcNames);
+
+ var testParentResource = testResources.Single(x => x.ResourceClaim.ResourceName == parentRcNames.First());
+
+ var test1ChildResourceClaim = $"{childRcNames.First()}-{parentRcNames.First()}";
+ var test2ChildResourceClaim = $"{childRcNames.Last()}-{parentRcNames.First()}";
+
+ using var securityContext = TestContext;
+ var testChildResource1ToEdit = securityContext.ResourceClaims.Single(x => x.ResourceName == test1ChildResourceClaim && x.ParentResourceClaimId == testParentResource.ResourceClaim.ResourceClaimId);
+ var testChildResource2NotToEdit = securityContext.ResourceClaims.Single(x => x.ResourceName == test2ChildResourceClaim && x.ParentResourceClaimId == testParentResource.ResourceClaim.ResourceClaimId);
+ var editedResource = new ResourceClaim
+ {
+ Id = testChildResource1ToEdit.ResourceClaimId,
+ Name = testChildResource1ToEdit.ResourceName,
+ Actions = new List
+ {
+ new ResourceClaimAction{ Name = "Create", Enabled = false },
+ new ResourceClaimAction{ Name = "Read", Enabled = false },
+ new ResourceClaimAction{ Name = "Update", Enabled = true },
+ new ResourceClaimAction{ Name = "Delete", Enabled = true }
+ }
+ };
+
+ var editResourceOnClaimSetModel = new Mock();
+ editResourceOnClaimSetModel.Setup(x => x.ClaimSetId).Returns(testClaimSet.ClaimSetId);
+ editResourceOnClaimSetModel.Setup(x => x.ResourceClaim).Returns(editedResource);
+
+ var command = new EditResourceOnClaimSetCommand(securityContext);
+ command.Execute(editResourceOnClaimSetModel.Object);
+
+ var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId);
+
+ var resultParentResourceClaim = resourceClaimsForClaimSet.Single(x => x.Id == testParentResource.ResourceClaim.ResourceClaimId);
+ resultParentResourceClaim.Actions.ShouldNotBeNull();
+ resultParentResourceClaim.Actions.Count.ShouldBe(1);
+ resultParentResourceClaim.Actions.Any(x => x.Name.Equals("Create")).ShouldBe(true);
+
+ var resultChildResourceClaim1 =
+ resultParentResourceClaim.Children.Single(x => x.Id == editedResource.Id);
+
+ resultChildResourceClaim1.Actions.ShouldNotBeNull();
+ resultChildResourceClaim1.Actions.Count.ShouldBe(2);
+ resultChildResourceClaim1.Actions.All(x =>
+ editedResource.Actions.Any(y => x.Name.Equals(y.Name) && x.Enabled.Equals(y.Enabled))).ShouldBe(true);
+
+ var resultChildResourceClaim2 =
+ resultParentResourceClaim.Children.Single(x => x.Id == testChildResource2NotToEdit.ResourceClaimId);
+
+ resultParentResourceClaim.Actions.ShouldNotBeNull();
+ resultParentResourceClaim.Actions.Count.ShouldBe(1);
+ resultParentResourceClaim.Actions.Any(x => x.Name.Equals("Create")).ShouldBe(true);
+ }
+
+ [Test]
+ public void ShouldEditGrandChildResourcesOnClaimSet()
+ {
+ var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" };
+ Save(testClaimSet);
+
+ var grandChildRcNamePrefix = "GrandChildRc";
+
+ var parentRcNames = UniqueNameList("ParentRc", 1);
+ var childRcNames = UniqueNameList("ChildRc", 2);
+ var grandChildRcNames = UniqueNameList(grandChildRcNamePrefix, 1);
+ var testResources = SetupClaimSetResourceClaimActions(testClaimSet, parentRcNames, childRcNames, grandChildRcNames);
+
+ var testParentResource = testResources.Single(x => x.ResourceClaim.ResourceName == parentRcNames.First());
+
+ var test1ChildResourceClaim = $"{childRcNames.First()}-{parentRcNames.First()}";
+ var test2ChildResourceClaim = $"{childRcNames.Last()}-{parentRcNames.First()}";
+
+ using var securityContext = TestContext;
+
+ var grandChildResources = securityContext.ResourceClaims.Where(rc => rc.ClaimName.StartsWith(grandChildRcNamePrefix)).ToArray();
+
+ // there must be at least two grandchild ResourceClaims so that one can be edited while the other remains unchanged
+ grandChildResources.ShouldNotBeNull();
+ grandChildResources.Count().ShouldBeGreaterThanOrEqualTo(2);
+
+ var grandChildRCToEdit = grandChildResources[0];
+ var grandChildRCNotToEdit = grandChildResources[1];
+
+ var editedResource = new ResourceClaim
+ {
+ Id = grandChildRCToEdit.ResourceClaimId,
+ Name = grandChildRCToEdit.ResourceName,
+ Actions = new List
+ {
+ new ResourceClaimAction{ Name = "Create", Enabled = false },
+ new ResourceClaimAction{ Name = "Read", Enabled = false },
+ new ResourceClaimAction{ Name = "Update", Enabled = true },
+ new ResourceClaimAction{ Name = "Delete", Enabled = true }
+ }
+ };
+
+ var editResourceOnClaimSetModel = new Mock();
+ editResourceOnClaimSetModel.Setup(x => x.ClaimSetId).Returns(testClaimSet.ClaimSetId);
+ editResourceOnClaimSetModel.Setup(x => x.ResourceClaim).Returns(editedResource);
+
+ var command = new EditResourceOnClaimSetCommand(securityContext);
+ command.Execute(editResourceOnClaimSetModel.Object);
+
+
+ var resourceClaims = securityContext.ClaimSetResourceClaimActions.Where(rca => rca.ClaimSetId == testClaimSet.ClaimSetId);
+
+
+ var editedGrandChildResourceClaimAction = resourceClaims.Where(rca => rca.ResourceClaimId == grandChildRCToEdit.ResourceClaimId).ToList();
+
+ editedGrandChildResourceClaimAction.ShouldNotBeNull();
+ editedGrandChildResourceClaimAction.Count.ShouldBe(2);
+ editedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Update")).ShouldBe(true);
+ editedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Delete")).ShouldBe(true);
+ editedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Create")).ShouldBe(false);
+ editedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Read")).ShouldBe(false);
+
+
+ var notEditedGrandChildResourceClaimAction = resourceClaims.Where(rca => rca.ResourceClaimId == grandChildRCNotToEdit.ResourceClaimId).ToList();
+
+ notEditedGrandChildResourceClaimAction.ShouldNotBeNull();
+ notEditedGrandChildResourceClaimAction.Count.ShouldBe(1);
+ notEditedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Create")).ShouldBe(true);
+ notEditedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Read")).ShouldBe(false);
+ notEditedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Update")).ShouldBe(false);
+ notEditedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Delete")).ShouldBe(false);
+
+ }
+
+
+ [Test]
+ public void ShouldAddParentResourceToClaimSet()
+ {
+ var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" };
+ Save(testClaimSet);
+
+ var parentRcNames = UniqueNameList("Parent", 1);
+ var testResources = SetupResourceClaims(parentRcNames, UniqueNameList("child", 1));
+ var testResourceToAdd = testResources.Single(x => x.ResourceName == parentRcNames.First());
+ var resourceToAdd = new ResourceClaim()
+ {
+ Id = testResourceToAdd.ResourceClaimId,
+ Name = testResourceToAdd.ResourceName,
+ Actions = new List
+ {
+ new ResourceClaimAction{ Name = "Create", Enabled = true },
+ new ResourceClaimAction{ Name = "Read", Enabled = false },
+ new ResourceClaimAction{ Name = "Update", Enabled = true },
+ new ResourceClaimAction{ Name = "Delete", Enabled = false }
+ }
+ };
+ var existingResources = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId);
+
+ var editResourceOnClaimSetModel = new EditResourceOnClaimSetModel
+ {
+ ClaimSetId = testClaimSet.ClaimSetId,
+ ResourceClaim = resourceToAdd
+ };
+
+ using var securityContext = TestContext;
+ var command = new EditResourceOnClaimSetCommand(securityContext);
+ command.Execute(editResourceOnClaimSetModel);
+
+ var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId);
+
+ var resultResourceClaim1 = resourceClaimsForClaimSet.Single(x => x.Name == testResourceToAdd.ResourceName);
+
+ resultResourceClaim1.Actions.ShouldNotBeNull();
+ resultResourceClaim1.Actions.Count.ShouldBe(2);
+ resultResourceClaim1.Actions.All(x =>
+ resourceToAdd.Actions.Any(y => x.Name.Equals(y.Name) && x.Enabled.Equals(y.Enabled))).ShouldBe(true);
+ }
+
+ [Test]
+ public void ShouldAddChildResourcesToClaimSet()
+ {
+ var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" };
+ Save(testClaimSet);
+
+ var parentRcNames = UniqueNameList("Parent", 1);
+ var childRcNames = UniqueNameList("Child", 1);
+ var testResources = SetupResourceClaims(parentRcNames, childRcNames);
+
+ var testParentResource1 = testResources.Single(x => x.ResourceName == parentRcNames.First());
+ var childRcToTest = $"{childRcNames.First()}-{parentRcNames.First()}";
+
+ using var securityContext = TestContext;
+ var testChildResource1ToAdd = securityContext.ResourceClaims.Single(x => x.ResourceName == childRcToTest && x.ParentResourceClaimId == testParentResource1.ResourceClaimId);
+ var resourceToAdd = new ResourceClaim()
+ {
+ Id = testChildResource1ToAdd.ResourceClaimId,
+ Name = testChildResource1ToAdd.ResourceName,
+ Actions = new List
+ {
+ new ResourceClaimAction{ Name = "Create", Enabled = true },
+ new ResourceClaimAction{ Name = "Read", Enabled = false },
+ new ResourceClaimAction{ Name = "Update", Enabled = true },
+ new ResourceClaimAction{ Name = "Delete", Enabled = false }
+ }
+ };
+ var existingResources = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId);
+
+ var editResourceOnClaimSetModel = new EditResourceOnClaimSetModel
+ {
+ ClaimSetId = testClaimSet.ClaimSetId,
+ ResourceClaim = resourceToAdd
+ };
+
+ var command = new EditResourceOnClaimSetCommand(securityContext);
+ command.Execute(editResourceOnClaimSetModel);
+
+ var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId);
+
+ var resultChildResourceClaim1 =
+ resourceClaimsForClaimSet.Single(x => x.Name == testChildResource1ToAdd.ResourceName);
+
+ resultChildResourceClaim1.Actions.ShouldNotBeNull();
+ resultChildResourceClaim1.Actions.Count.ShouldBe(2);
+ resultChildResourceClaim1.Actions.All(x =>
+ resourceToAdd.Actions.Any(y => x.Name.Equals(y.Name) && x.Enabled.Equals(y.Enabled))).ShouldBe(true);
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandV53ServiceTests.cs
deleted file mode 100644
index 7394caf57..000000000
--- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandV53ServiceTests.cs
+++ /dev/null
@@ -1,227 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// Licensed to the Ed-Fi Alliance under one or more agreements.
-// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
-// See the LICENSE and NOTICES files in the project root for more information.
-extern alias Compatability;
-
-using System;
-using System.Linq;
-using NUnit.Framework;
-using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor;
-using Shouldly;
-using Moq;
-using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application;
-using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet;
-using ResourceClaim = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ResourceClaim;
-
-namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests;
-
-[TestFixture]
-public class EditResourceOnClaimSetCommandV53ServiceTests : SecurityData53TestBase
-{
- [Test]
- public void ShouldEditParentResourcesOnClaimSet()
- {
- var testApplication = new Application
- {
- ApplicationName = $"Test Application {DateTime.Now:O}"
- };
- Save(testApplication);
-
- var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", Application = testApplication };
- Save(testClaimSet);
-
- var testResources = SetupParentResourceClaimsWithChildren(testClaimSet, testApplication);
-
- var testResource1ToEdit = testResources.Select(x => x.ResourceClaim).Single(x => x.ResourceName == "TestParentResourceClaim1");
- var testResource2ToNotEdit = testResources.Select(x => x.ResourceClaim).Single(x => x.ResourceName == "TestParentResourceClaim2");
-
- var editedResource = new ResourceClaim
- {
- Id = testResource1ToEdit.ResourceClaimId,
- Name = testResource1ToEdit.ResourceName,
- Create = false,
- Read = false,
- Update = true,
- Delete = true
- };
-
- var editResourceOnClaimSetModel = new Mock();
- editResourceOnClaimSetModel.Setup(x => x.ClaimSetId).Returns(testClaimSet.ClaimSetId);
- editResourceOnClaimSetModel.Setup(x => x.ResourceClaim).Returns(editedResource);
-
- using var securityContext = TestContext;
- var command = new EditResourceOnClaimSetCommandV53Service(securityContext);
- command.Execute(editResourceOnClaimSetModel.Object);
-
- var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId);
-
- var resultResourceClaim1 = resourceClaimsForClaimSet.Single(x => x.Id == editedResource.Id);
-
- resultResourceClaim1.Create.ShouldBe(editedResource.Create);
- resultResourceClaim1.Read.ShouldBe(editedResource.Read);
- resultResourceClaim1.Update.ShouldBe(editedResource.Update);
- resultResourceClaim1.Delete.ShouldBe(editedResource.Delete);
-
- var resultResourceClaim2 = resourceClaimsForClaimSet.Single(x => x.Id == testResource2ToNotEdit.ResourceClaimId);
-
- resultResourceClaim2.Create.ShouldBe(true);
- resultResourceClaim2.Read.ShouldBe(false);
- resultResourceClaim2.Update.ShouldBe(false);
- resultResourceClaim2.Delete.ShouldBe(false);
- }
-
- [Test]
- public void ShouldEditChildResourcesOnClaimSet()
- {
- var testApplication = new Application
- {
- ApplicationName = $"Test Application {DateTime.Now:O}"
- };
- Save(testApplication);
-
- var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", Application = testApplication };
- Save(testClaimSet);
-
- var testResources = SetupParentResourceClaimsWithChildren(testClaimSet, testApplication);
-
- var testParentResource = testResources.Single(x => x.ResourceClaim.ResourceName == "TestParentResourceClaim1");
-
- using var securityContext = TestContext;
- var testChildResource1ToEdit = securityContext.ResourceClaims.Single(x => x.ResourceName == "TestChildResourceClaim1" && x.ParentResourceClaimId == testParentResource.ResourceClaim.ResourceClaimId);
- var testChildResource2NotToEdit = securityContext.ResourceClaims.Single(x => x.ResourceName == "TestChildResourceClaim2" && x.ParentResourceClaimId == testParentResource.ResourceClaim.ResourceClaimId);
-
- var editedResource = new ResourceClaim
- {
- Id = testChildResource1ToEdit.ResourceClaimId,
- Name = testChildResource1ToEdit.ResourceName,
- Create = false,
- Read = false,
- Update = true,
- Delete = true
- };
-
- var editResourceOnClaimSetModel = new Mock();
- editResourceOnClaimSetModel.Setup(x => x.ClaimSetId).Returns(testClaimSet.ClaimSetId);
- editResourceOnClaimSetModel.Setup(x => x.ResourceClaim).Returns(editedResource);
-
- var command = new EditResourceOnClaimSetCommandV53Service(securityContext);
- command.Execute(editResourceOnClaimSetModel.Object);
-
- var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId);
-
- var resultParentResourceClaim = resourceClaimsForClaimSet.Single(x => x.Id == testParentResource.ResourceClaim.ResourceClaimId);
- resultParentResourceClaim.Create.ShouldBe(true);
- resultParentResourceClaim.Read.ShouldBe(false);
- resultParentResourceClaim.Update.ShouldBe(false);
- resultParentResourceClaim.Delete.ShouldBe(false);
-
- var resultChildResourceClaim1 =
- resultParentResourceClaim.Children.Single(x => x.Id == editedResource.Id);
-
- resultChildResourceClaim1.Create.ShouldBe(editedResource.Create);
- resultChildResourceClaim1.Read.ShouldBe(editedResource.Read);
- resultChildResourceClaim1.Update.ShouldBe(editedResource.Update);
- resultChildResourceClaim1.Delete.ShouldBe(editedResource.Delete);
-
- var resultChildResourceClaim2 =
- resultParentResourceClaim.Children.Single(x => x.Id == testChildResource2NotToEdit.ResourceClaimId);
-
- resultChildResourceClaim2.Create.ShouldBe(true);
- resultChildResourceClaim2.Read.ShouldBe(false);
- resultChildResourceClaim2.Update.ShouldBe(false);
- resultChildResourceClaim2.Delete.ShouldBe(false);
- }
-
- [Test]
- public void ShouldAddParentResourceToClaimSet()
- {
- var testApplication = new Application
- {
- ApplicationName = $"Test Application {DateTime.Now:O}"
- };
- Save(testApplication);
-
- var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", Application = testApplication };
- Save(testClaimSet);
-
- var testResources = SetupResourceClaims(testApplication);
- var testResourceToAdd = testResources.Single(x => x.ResourceName == "TestParentResourceClaim1");
- var resourceToAdd = new ResourceClaim()
- {
- Id = testResourceToAdd.ResourceClaimId,
- Name = testResourceToAdd.ResourceName,
- Create = true,
- Read = false,
- Update = true,
- Delete = false
- };
- var existingResources = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId);
-
- var editResourceOnClaimSetModel = new EditResourceOnClaimSetModel
- {
- ClaimSetId = testClaimSet.ClaimSetId,
- ResourceClaim = resourceToAdd
- };
-
- using var securityContext = TestContext;
- var command = new EditResourceOnClaimSetCommandV53Service(securityContext);
- command.Execute(editResourceOnClaimSetModel);
-
- var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId);
-
- var resultResourceClaim1 = resourceClaimsForClaimSet.Single(x => x.Name == testResourceToAdd.ResourceName);
-
- resultResourceClaim1.Create.ShouldBe(resourceToAdd.Create);
- resultResourceClaim1.Read.ShouldBe(resourceToAdd.Read);
- resultResourceClaim1.Update.ShouldBe(resourceToAdd.Update);
- resultResourceClaim1.Delete.ShouldBe(resourceToAdd.Delete);
- }
-
- [Test]
- public void ShouldAddChildResourcesToClaimSet()
- {
- var testApplication = new Application
- {
- ApplicationName = $"Test Application {DateTime.Now:O}"
- };
- Save(testApplication);
-
- var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", Application = testApplication };
- Save(testClaimSet);
-
- var testResources = SetupResourceClaims(testApplication);
- var testParentResource1 = testResources.Single(x => x.ResourceName == "TestParentResourceClaim1");
-
- using var securityContext = TestContext;
- var testChildResource1ToAdd = securityContext.ResourceClaims.Single(x => x.ResourceName == "TestChildResourceClaim1" && x.ParentResourceClaimId == testParentResource1.ResourceClaimId);
- var resourceToAdd = new ResourceClaim()
- {
- Id = testChildResource1ToAdd.ResourceClaimId,
- Name = testChildResource1ToAdd.ResourceName,
- Create = true,
- Read = false,
- Update = true,
- Delete = false
- };
- var existingResources = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId);
-
- var editResourceOnClaimSetModel = new EditResourceOnClaimSetModel
- {
- ClaimSetId = testClaimSet.ClaimSetId,
- ResourceClaim = resourceToAdd
- };
- var command = new EditResourceOnClaimSetCommandV53Service(securityContext);
- command.Execute(editResourceOnClaimSetModel);
-
- var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId);
-
- var resultChildResourceClaim1 =
- resourceClaimsForClaimSet.Single(x => x.Name == testChildResource1ToAdd.ResourceName);
-
- resultChildResourceClaim1.Create.ShouldBe(resourceToAdd.Create);
- resultChildResourceClaim1.Read.ShouldBe(resourceToAdd.Read);
- resultChildResourceClaim1.Update.ShouldBe(resourceToAdd.Update);
- resultChildResourceClaim1.Delete.ShouldBe(resourceToAdd.Delete);
- }
-}
diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetAllClaimSetsQueryV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetAllClaimSetsQueryV53ServiceTests.cs
deleted file mode 100644
index f5e122e13..000000000
--- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetAllClaimSetsQueryV53ServiceTests.cs
+++ /dev/null
@@ -1,109 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// Licensed to the Ed-Fi Alliance under one or more agreements.
-// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
-// See the LICENSE and NOTICES files in the project root for more information.
-extern alias Compatability;
-
-using System;
-using System.Linq;
-using EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor;
-using NUnit.Framework;
-using Shouldly;
-
-using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet;
-using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application;
-using EdFi.Ods.AdminApi.Infrastructure;
-
-namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests;
-
-[TestFixture]
-public class GetAllClaimSetsQueryV53ServiceTests : SecurityData53TestBase
-{
- public GetAllClaimSetsQueryV53ServiceTests()
- {
- SeedSecurityContextOnFixtureSetup = true;
- }
-
- [Test]
- public void Should_Retrieve_ClaimSetNames()
- {
- var application = new Application
- {
- ApplicationName = $"Test Application {DateTime.Now:O}"
- };
- Save(application);
-
- var claimSet1 = GetClaimSet(application);
- var claimSet2 = GetClaimSet(application);
- Save(claimSet1, claimSet2);
-
- var claimSetNames = Transaction(securityContext =>
- {
- var query = new GetAllClaimSetsQueryV53Service(securityContext, Testing.GetAppSettings());
- return query.Execute().Select(x => x.Name).ToArray();
- });
-
- claimSetNames.ShouldContain(claimSet1.ClaimSetName);
- claimSetNames.ShouldContain(claimSet2.ClaimSetName);
- }
-
- [Test]
- public void Should_Retrieve_ClaimSetNames_with_offset_and_limit()
- {
- var application = new Application
- {
- ApplicationName = $"Test Application {DateTime.Now:O}"
- };
- Save(application);
-
- var claimSet1 = GetClaimSet(application);
- var claimSet2 = GetClaimSet(application);
- var claimSet3 = GetClaimSet(application);
- var claimSet4 = GetClaimSet(application);
- var claimSet5 = GetClaimSet(application);
-
- Save(claimSet1, claimSet2, claimSet3, claimSet4, claimSet5);
-
- var commonQueryParams = new CommonQueryParams(0, 2);
- var claimSetNames = Transaction(securityContext =>
- {
- var query = new GetAllClaimSetsQueryV53Service(securityContext, Testing.GetAppSettings());
- return query.Execute(commonQueryParams).Select(x => x.Name).ToArray();
- });
-
- claimSetNames.Length.ShouldBe(2);
- claimSetNames.ShouldContain(claimSet1.ClaimSetName);
- claimSetNames.ShouldContain(claimSet2.ClaimSetName);
-
- commonQueryParams.Offset = 2;
- claimSetNames = Transaction(securityContext =>
- {
- var query = new GetAllClaimSetsQueryV53Service(securityContext, Testing.GetAppSettings());
- return query.Execute(commonQueryParams).Select(x => x.Name).ToArray();
- });
-
- claimSetNames.Length.ShouldBe(2);
- claimSetNames.ShouldContain(claimSet3.ClaimSetName);
- claimSetNames.ShouldContain(claimSet4.ClaimSetName);
-
- commonQueryParams.Offset = 4;
- claimSetNames = Transaction(securityContext =>
- {
- var query = new GetAllClaimSetsQueryV53Service(securityContext, Testing.GetAppSettings());
- return query.Execute(commonQueryParams).Select(x => x.Name).ToArray();
- });
-
- claimSetNames.Length.ShouldBe(1);
- claimSetNames.ShouldContain(claimSet5.ClaimSetName);
- }
-
- private static int _claimSetId = 0;
- private static ClaimSet GetClaimSet(Application application)
- {
- return new ClaimSet
- {
- Application = application,
- ClaimSetName = $"Test Claim Set {_claimSetId++} - {DateTime.Now:O}"
- };
- }
-}
diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryV6ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryTests.cs
similarity index 91%
rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryV6ServiceTests.cs
rename to Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryTests.cs
index 5fc304524..2ade65c63 100644
--- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryV6ServiceTests.cs
+++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryTests.cs
@@ -9,15 +9,16 @@
using NUnit.Framework;
using Shouldly;
using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor;
-using Application = EdFi.Security.DataAccess.Models.Application;
+using Application = EdFi.Admin.DataAccess.Models.Application;
using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet;
using VendorApplication = EdFi.Admin.DataAccess.Models.Application;
using EdFi.Ods.AdminApi.Infrastructure;
+using EdFi.Ods.AdminApi.Common.Infrastructure;
namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests;
[TestFixture]
-public class GetApplicationsByClaimSetIdQueryV6ServiceTests : SecurityDataTestBase
+public class GetApplicationsByClaimSetIdQueryTests : SecurityDataTestBase
{
[TestCase(1)]
[TestCase(3)]
@@ -74,13 +75,6 @@ public void ShouldGetClaimSetApplicationsCount(int applicationsCount)
private IReadOnlyCollection SetupApplicationWithClaimSets(
string applicationName = "TestApplicationName", int claimSetCount = 5)
{
- var testApplication = new Application
- {
- ApplicationName = applicationName
- };
-
- Save(testApplication);
-
var testClaimSetNames = Enumerable.Range(1, claimSetCount)
.Select((x, index) => $"TestClaimSetName{index:N}")
.ToArray();
@@ -88,8 +82,7 @@ private IReadOnlyCollection SetupApplicationWithClaimSets(
var testClaimSets = testClaimSetNames
.Select(x => new ClaimSet
{
- ClaimSetName = x,
- Application = testApplication
+ ClaimSetName = x
})
.ToArray();
diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryTests.cs
new file mode 100644
index 000000000..f4876194f
--- /dev/null
+++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryTests.cs
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: Apache-2.0
+// Licensed to the Ed-Fi Alliance under one or more agreements.
+// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
+// See the LICENSE and NOTICES files in the project root for more information.
+
+using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor;
+using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling;
+using EdFi.Security.DataAccess.Contexts;
+using NUnit.Framework;
+using Shouldly;
+using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet;
+
+namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests;
+
+[TestFixture]
+public class GetClaimSetByIdQueryTests : SecurityDataTestBase
+{
+ [Test]
+ public void ShouldGetClaimSetById()
+ {
+ var testClaimSet = new ClaimSet
+ {
+ ClaimSetName = "TestClaimSet",
+ ForApplicationUseOnly = false,
+ IsEdfiPreset = false
+ };
+ Save(testClaimSet);
+
+ using var securityContext = TestContext;
+ var query = new GetClaimSetByIdQuery(securityContext);
+ var result = query.Execute(testClaimSet.ClaimSetId);
+ result.Name.ShouldBe(testClaimSet.ClaimSetName);
+ result.Id.ShouldBe(testClaimSet.ClaimSetId);
+ result.IsEditable.ShouldBe(true);
+ }
+
+ [Test]
+ public void ShouldGetNonEditableClaimSetById()
+ {
+ var systemReservedClaimSet = new ClaimSet
+ {
+ ClaimSetName = "SystemReservedClaimSet",
+ ForApplicationUseOnly = true
+ };
+ Save(systemReservedClaimSet);
+
+ var edfiPresetClaimSet = new ClaimSet
+ {
+ ClaimSetName = "EdfiPresetClaimSet",
+ ForApplicationUseOnly = false,
+ IsEdfiPreset = true
+ };
+ Save(edfiPresetClaimSet);
+
+ using var securityContext = TestContext;
+ var query = new GetClaimSetByIdQuery(securityContext);
+ var result = query.Execute(systemReservedClaimSet.ClaimSetId);
+ result.Name.ShouldBe(systemReservedClaimSet.ClaimSetName);
+ result.Id.ShouldBe(systemReservedClaimSet.ClaimSetId);
+ result.IsEditable.ShouldBe(false);
+
+ result = query.Execute(edfiPresetClaimSet.ClaimSetId);
+
+ result.Name.ShouldBe(edfiPresetClaimSet.ClaimSetName);
+ result.Id.ShouldBe(edfiPresetClaimSet.ClaimSetId);
+ result.IsEditable.ShouldBe(false);
+ }
+
+ [Test]
+ public void ShouldThrowExceptionForNonExistingClaimSetId()
+ {
+ const int NonExistingClaimSetId = 1;
+
+ using var securityContext = TestContext;
+ EnsureZeroClaimSets(securityContext);
+
+ var exception = Assert.Throws>(() =>
+ {
+ var query = new GetClaimSetByIdQuery(securityContext);
+ query.Execute(NonExistingClaimSetId);
+ });
+ exception.ShouldNotBeNull();
+ exception.Message.ShouldBe("Not found: claimset with ID 1. It may have been recently deleted.");
+
+ static void EnsureZeroClaimSets(ISecurityContext database)
+ {
+ foreach (var entity in database.ClaimSets)
+ database.ClaimSets.Remove(entity);
+ database.SaveChanges();
+ }
+ }
+}
diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryV53ServiceTests.cs
deleted file mode 100644
index 6b48973d4..000000000
--- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryV53ServiceTests.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// Licensed to the Ed-Fi Alliance under one or more agreements.
-// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
-// See the LICENSE and NOTICES files in the project root for more information.
-extern alias Compatability;
-
-using System;
-using NUnit.Framework;
-using Shouldly;
-using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor;
-using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling;
-using System.Net;
-using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts;
-
-using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet;
-using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application;
-
-namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests;
-
-[TestFixture]
-public class GetClaimSetByIdQueryV53ServiceTests : SecurityData53TestBase
-{
- [Test]
- public void ShouldGetClaimSetById()
- {
- var testApplication = new Application
- {
- ApplicationName = $"Test Application {DateTime.Now:O}"
- };
- Save(testApplication);
-
- var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", Application = testApplication };
- Save(testClaimSet);
-
- using var securityContext = TestContext;
- var query = new GetClaimSetByIdQueryV53Service(securityContext);
- var result = query.Execute(testClaimSet.ClaimSetId);
- result.Name.ShouldBe(testClaimSet.ClaimSetName);
- result.Id.ShouldBe(testClaimSet.ClaimSetId);
- }
-
- [Test]
- public void ShouldThrowExceptionForNonExistingClaimSetId()
- {
- const int NonExistingClaimSetId = 1;
-
- using var securityContext = TestContext;
- EnsureZeroClaimSets(securityContext);
- var adminApiException = Assert.Throws(() =>
- {
- var query = new GetClaimSetByIdQueryV53Service(securityContext);
- query.Execute(NonExistingClaimSetId);
- });
- adminApiException.ShouldNotBeNull();
- adminApiException.StatusCode.ShouldBe(HttpStatusCode.NotFound);
- adminApiException.Message.ShouldBe("No such claim set exists in the database.");
-
- static void EnsureZeroClaimSets(ISecurityContext database)
- {
- foreach (var entity in database.ClaimSets)
- database.ClaimSets.Remove(entity);
- database.SaveChanges();
- }
- }
-}
diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryTests.cs
similarity index 67%
rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryV53ServiceTests.cs
rename to Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryTests.cs
index eaa76f3f1..1833b5959 100644
--- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryV53ServiceTests.cs
+++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryTests.cs
@@ -2,101 +2,82 @@
// Licensed to the Ed-Fi Alliance under one or more agreements.
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.
-extern alias Compatability;
-using System.Collections.Generic;
-using System.Linq;
+using EdFi.Security.DataAccess.Models;
using NUnit.Framework;
using Shouldly;
-using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models;
-
-using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application;
-using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet;
-using ResourceClaim = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ResourceClaim;
-using Action = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Action;
+using System.Collections.Generic;
+using System.Linq;
+using Action = EdFi.Security.DataAccess.Models.Action;
using ActionName = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.Action;
+using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet;
+using ResourceClaim = EdFi.Security.DataAccess.Models.ResourceClaim;
namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests;
[TestFixture]
-public class GetResourcesByClaimSetIdQueryV53ServiceTests : SecurityData53TestBase
+public class GetResourcesByClaimSetIdQueryTests : SecurityDataTestBase
{
[Test]
public void ShouldGetParentResourcesByClaimSetId()
{
- var testApplication = new Application
- {
- ApplicationName = "TestApplicationName"
- };
+ var testClaimSets = SetupApplicationWithClaimSets().ToList();
+ var testResourceClaims = SetupParentResourceClaims(testClaimSets, UniqueNameList("ParentRc", 1));
- Save(testApplication);
- var testClaimSets = SetupApplicationWithClaimSets(testApplication).ToList();
- var testResourceClaims = SetupParentResourceClaims(testClaimSets, testApplication);
foreach (var testClaimSet in testClaimSets)
{
- var results = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId);
+ var results = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId).ToArray();
var testResourceClaimsForId =
testResourceClaims.Where(x => x.ClaimSet.ClaimSetId == testClaimSet.ClaimSetId).Select(x => x.ResourceClaim).ToArray();
- results.Count.ShouldBe(testResourceClaimsForId.Length);
+ results.Length.ShouldBe(testResourceClaimsForId.Length);
results.Select(x => x.Name).ShouldBe(testResourceClaimsForId.Select(x => x.ResourceName), true);
results.Select(x => x.Id).ShouldBe(testResourceClaimsForId.Select(x => x.ResourceClaimId), true);
- results.All(x => x.Create).ShouldBe(true);
+ results.All(x => x.Actions.All(x => x.Name.Equals("Create") && x.Enabled)).ShouldBe(true);
}
}
[Test]
public void ShouldGetSingleResourceByClaimSetIdAndResourceId()
{
- var testApplication = new Application
- {
- ApplicationName = "TestApplicationName"
- };
-
- Save(testApplication);
- var testClaimSets = SetupApplicationWithClaimSets(testApplication).ToList();
- var testResourceClaims = SetupParentResourceClaims(testClaimSets, testApplication);
+ var testClaimSets = SetupApplicationWithClaimSets().ToList();
+ var rcIds = UniqueNameList("ParentRc", 1);
+ var testResourceClaims = SetupParentResourceClaims(testClaimSets, rcIds);
foreach (var testClaimSet in testClaimSets)
{
+ var rcName = $"{rcIds.First()}{testClaimSet.ClaimSetName}";
var testResourceClaim =
- testResourceClaims.Single(x => x.ClaimSet.ClaimSetId == testClaimSet.ClaimSetId && x.ResourceClaim.ResourceName == "TestResourceClaim3.00").ResourceClaim;
+ testResourceClaims.Single(x => x.ClaimSet.ClaimSetId == testClaimSet.ClaimSetId && x.ResourceClaim.ResourceName == rcName).ResourceClaim;
var result = SingleResourceClaimForClaimSet(testClaimSet.ClaimSetId, testResourceClaim.ResourceClaimId);
result.Name.ShouldBe(testResourceClaim.ResourceName);
result.Id.ShouldBe(testResourceClaim.ResourceClaimId);
- result.Create.ShouldBe(true);
- result.Read.ShouldBe(false);
- result.Update.ShouldBe(false);
- result.Delete.ShouldBe(false);
+ result.Actions.All(x => x.Name.Equals("Create") && x.Enabled).ShouldBe(true);
+ result.Actions.All(x => x.Name.Equals("Read") && x.Enabled).ShouldBe(false);
+ result.Actions.All(x => x.Name.Equals("Update") && x.Enabled).ShouldBe(false);
+ result.Actions.All(x => x.Name.Equals("Delete") && x.Enabled).ShouldBe(false);
}
-
}
[Test]
public void ShouldGetParentResourcesWithChildrenByClaimSetId()
{
- var testApplication = new Application
- {
- ApplicationName = "TestApplicationName"
- };
-
- Save(testApplication);
- var testClaimSets = SetupApplicationWithClaimSets(testApplication);
- var testResourceClaims = SetupParentResourceClaimsWithChildren(testClaimSets, testApplication);
+ var testClaimSets = SetupApplicationWithClaimSets();
+ var testResourceClaims = SetupParentResourceClaimsWithChildren(testClaimSets);
using var securityContext = TestContext;
foreach (var testClaimSet in testClaimSets)
{
- var results = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId);
+ var results = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId).ToArray();
var testParentResourceClaimsForId =
testResourceClaims.Where(x => x.ClaimSet.ClaimSetId == testClaimSet.ClaimSetId && x.ResourceClaim.ParentResourceClaim == null).Select(x => x.ResourceClaim).ToArray();
- results.Count.ShouldBe(testParentResourceClaimsForId.Length);
+ results.Length.ShouldBe(testParentResourceClaimsForId.Length);
results.Select(x => x.Name).ShouldBe(testParentResourceClaimsForId.Select(x => x.ResourceName), true);
results.Select(x => x.Id).ShouldBe(testParentResourceClaimsForId.Select(x => x.ResourceClaimId), true);
- results.All(x => x.Create).ShouldBe(true);
+ results.All(x => x.Actions.All(x => x.Name.Equals("Create") && x.Enabled)).ShouldBe(true);
foreach (var testParentResourceClaim in testParentResourceClaimsForId)
{
@@ -105,93 +86,78 @@ public void ShouldGetParentResourcesWithChildrenByClaimSetId()
var parentResult = results.First(x => x.Id == testParentResourceClaim.ResourceClaimId);
parentResult.Children.Select(x => x.Name).ShouldBe(testChildren.Select(x => x.ResourceName), true);
parentResult.Children.Select(x => x.Id).ShouldBe(testChildren.Select(x => x.ResourceClaimId), true);
- parentResult.Children.All(x => x.Create).ShouldBe(true);
+ parentResult.Children.All(x => x.Actions.All(x => x.Name.Equals("Create") && x.Enabled)).ShouldBe(true);
}
+
}
}
[Test]
public void ShouldGetDefaultAuthorizationStrategiesForParentResourcesByClaimSetId()
{
- var testApplication = new Application
- {
- ApplicationName = "TestApplicationName"
- };
- Save(testApplication);
-
var testClaimSet = new ClaimSet
{
ClaimSetName = "TestClaimSet",
- Application = testApplication
};
Save(testClaimSet);
- var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies(testApplication).ToList();
- var testResourceClaims = SetupParentResourceClaims(new List { testClaimSet }, testApplication);
+ var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies().ToList();
+ var testResourceClaims = SetupParentResourceClaims(new List { testClaimSet }, UniqueNameList("ParentRc", 3));
var testAuthStrategies = SetupResourcesWithDefaultAuthorizationStrategies(appAuthorizationStrategies, testResourceClaims.ToList());
+
var results = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId).ToArray();
- results.Select(x => x.DefaultAuthStrategiesForCRUD[0].AuthorizationStrategies[0].AuthStrategyName).ShouldBe(testAuthStrategies.Select(x => x.AuthorizationStrategy.AuthorizationStrategyName), true);
+ results.Select(x => x.DefaultAuthorizationStrategiesForCRUD[0].AuthorizationStrategies.ToList().First().AuthStrategyName).ShouldBe(testAuthStrategies.Select(x => x.AuthorizationStrategies.Single()
+ .AuthorizationStrategy.AuthorizationStrategyName), true);
}
[Test]
public void ShouldGetDefaultAuthorizationStrategiesForSingleResourcesByClaimSetIdAndResourceId()
{
- var testApplication = new Application
- {
- ApplicationName = "TestApplicationName"
- };
- Save(testApplication);
-
var testClaimSet = new ClaimSet
{
ClaimSetName = "TestClaimSet",
- Application = testApplication
};
Save(testClaimSet);
- var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies(testApplication).ToList();
- var testResourceClaims = SetupParentResourceClaims(new List { testClaimSet }, testApplication);
+ var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies().ToList();
+ var rcIds = UniqueNameList("Parent", 1);
+ var testResourceClaims = SetupParentResourceClaims(new List { testClaimSet }, rcIds);
var testAuthStrategies = SetupResourcesWithDefaultAuthorizationStrategies(appAuthorizationStrategies, testResourceClaims.ToList());
+ var rcName = $"{rcIds.First()}{testClaimSet.ClaimSetName}";
var testResourceClaim =
- testResourceClaims.Single(x => x.ClaimSet.ClaimSetId == testClaimSet.ClaimSetId && x.ResourceClaim.ResourceName == "TestResourceClaim3.00").ResourceClaim;
+ testResourceClaims.Single(x => x.ClaimSet.ClaimSetId == testClaimSet.ClaimSetId && x.ResourceClaim.ResourceName == rcName).ResourceClaim;
var testAuthStrategy = testAuthStrategies.Single(x =>
- x.ResourceClaim.ResourceClaimId == testResourceClaim.ResourceClaimId && x.Action.ActionName == ActionName.Create.Value).AuthorizationStrategy;
+ x.ResourceClaim.ResourceClaimId == testResourceClaim.ResourceClaimId && x.Action.ActionName == ActionName.Create.Value)
+ .AuthorizationStrategies.Single().AuthorizationStrategy;
var result = SingleResourceClaimForClaimSet(testClaimSet.ClaimSetId, testResourceClaim.ResourceClaimId);
result.Name.ShouldBe(testResourceClaim.ResourceName);
result.Id.ShouldBe(testResourceClaim.ResourceClaimId);
- result.Create.ShouldBe(true);
- result.Read.ShouldBe(false);
- result.Update.ShouldBe(false);
- result.Delete.ShouldBe(false);
- result.DefaultAuthStrategiesForCRUD[0].AuthorizationStrategies[0].AuthStrategyName.ShouldBe(testAuthStrategy.DisplayName);
+ result.Actions.All(x => x.Name.Equals("Create") && x.Enabled).ShouldBe(true);
+ result.Actions.All(x => x.Name.Equals("Read") && x.Enabled).ShouldBe(false);
+ result.Actions.All(x => x.Name.Equals("Update") && x.Enabled).ShouldBe(false);
+ result.Actions.All(x => x.Name.Equals("Delete") && x.Enabled).ShouldBe(false);
+ result.DefaultAuthorizationStrategiesForCRUD[0].AuthorizationStrategies.ToList().First().AuthStrategyName.ShouldBe(testAuthStrategy.DisplayName);
+
}
[Test]
public void ShouldGetDefaultAuthorizationStrategiesForParentResourcesWithChildrenByClaimSetId()
{
- var testApplication = new Application
- {
- ApplicationName = "TestApplicationName"
- };
- Save(testApplication);
-
var testClaimSet = new ClaimSet
{
ClaimSetName = "TestClaimSet",
- Application = testApplication
};
Save(testClaimSet);
- var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies(testApplication).ToList();
+ var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies().ToList();
- var testResourceClaims = SetupParentResourceClaimsWithChildren(new List { testClaimSet }, testApplication);
+ var testResourceClaims = SetupParentResourceClaimsWithChildren(new List { testClaimSet });
var testAuthStrategies = SetupResourcesWithDefaultAuthorizationStrategies(appAuthorizationStrategies, testResourceClaims.ToList());
-
var results = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId).ToArray();
var testParentResourceClaimsForId =
@@ -202,7 +168,8 @@ public void ShouldGetDefaultAuthorizationStrategiesForParentResourcesWithChildre
var testAuthStrategiesForParents =
testAuthStrategies.Where(x => x.ResourceClaim.ParentResourceClaim == null);
- results.Select(x => x.DefaultAuthStrategiesForCRUD[0].AuthorizationStrategies[0].AuthStrategyName).ShouldBe(testAuthStrategiesForParents.Select(x => x.AuthorizationStrategy.AuthorizationStrategyName), true);
+ results.Select(x => x.DefaultAuthorizationStrategiesForCRUD[0].AuthorizationStrategies.ToList().First().AuthStrategyName).ShouldBe(testAuthStrategiesForParents.Select(x =>
+ x.AuthorizationStrategies.Single().AuthorizationStrategy.AuthorizationStrategyName), true);
foreach (var testParentResourceClaim in testParentResourceClaimsForId)
{
@@ -210,12 +177,12 @@ public void ShouldGetDefaultAuthorizationStrategiesForParentResourcesWithChildre
var testAuthStrategiesForChildren =
testAuthStrategies.Where(x =>
x.ResourceClaim.ParentResourceClaimId == testParentResourceClaim.ResourceClaimId);
- parentResult.Children.Select(x => x.DefaultAuthStrategiesForCRUD[0].AuthorizationStrategies[0].AuthStrategyName).ShouldBe(testAuthStrategiesForChildren.Select(x => x.AuthorizationStrategy.AuthorizationStrategyName), true);
+ parentResult.Children.Select(x => x.DefaultAuthorizationStrategiesForCRUD[0].AuthorizationStrategies.ToList().First().AuthStrategyName).ShouldBe(testAuthStrategiesForChildren.Select(x => x.AuthorizationStrategies.Single()
+ .AuthorizationStrategy.AuthorizationStrategyName), true);
}
-
}
- private IReadOnlyCollection SetupApplicationWithClaimSets(Application testApplication, int claimSetCount = 5)
+ private IReadOnlyCollection SetupApplicationWithClaimSets(int claimSetCount = 5)
{
var testClaimSetNames = Enumerable.Range(1, claimSetCount)
.Select((x, index) => $"TestClaimSetName{index:N}")
@@ -225,7 +192,6 @@ private IReadOnlyCollection SetupApplicationWithClaimSets(Application
.Select(x => new ClaimSet
{
ClaimSetName = x,
- Application = testApplication
})
.ToArray();
@@ -234,26 +200,25 @@ private IReadOnlyCollection SetupApplicationWithClaimSets(Application
return testClaimSets;
}
- private IReadOnlyCollection SetupParentResourceClaims(IEnumerable testClaimSets, Application testApplication, int resourceClaimCount = 5)
+ private IReadOnlyCollection SetupParentResourceClaims(IEnumerable testClaimSets, IList resouceClaimsIds)
{
- var claimSetResourceClaims = new List();
+ var claimSetResourceClaims = new List();
foreach (var claimSet in testClaimSets)
{
- foreach (var index in Enumerable.Range(1, resourceClaimCount))
+ foreach (var index in resouceClaimsIds)
{
+ var rcName = $"{index}{claimSet.ClaimSetName}";
var resourceClaim = new ResourceClaim
{
- ClaimName = $"TestResourceClaim{index:N}",
- DisplayName = $"TestResourceClaim{index:N}",
- ResourceName = $"TestResourceClaim{index:N}",
- Application = testApplication
+ ClaimName = rcName,
+ ResourceName = rcName,
};
var action = new Action
{
ActionName = ActionName.Create.Value,
ActionUri = "create"
};
- var claimSetResourceClaim = new ClaimSetResourceClaim
+ var claimSetResourceClaim = new ClaimSetResourceClaimAction
{
ResourceClaim = resourceClaim, Action = action, ClaimSet = claimSet
};
@@ -266,7 +231,7 @@ private IReadOnlyCollection SetupParentResourceClaims(IEn
return claimSetResourceClaims;
}
- private IReadOnlyCollection SetupParentResourceClaimsWithChildren(IEnumerable testClaimSets, Application testApplication, int resourceClaimCount = 5, int childResourceClaimCount = 3)
+ private IReadOnlyCollection SetupParentResourceClaimsWithChildren(IEnumerable testClaimSets, int resourceClaimCount = 5, int childResourceClaimCount = 1)
{
var parentResourceClaims = new List();
var childResourceClaims = new List();
@@ -275,19 +240,15 @@ private IReadOnlyCollection SetupParentResourceClaimsWith
var resourceClaim = new ResourceClaim
{
ClaimName = $"TestParentResourceClaim{parentIndex:N}",
- DisplayName = $"TestParentResourceClaim{parentIndex:N}",
ResourceName = $"TestParentResourceClaim{parentIndex:N}",
- Application = testApplication
};
parentResourceClaims.Add(resourceClaim);
childResourceClaims.AddRange(Enumerable.Range(1, childResourceClaimCount)
.Select(childIndex => new ResourceClaim
{
- ClaimName = $"TestChildResourceClaim{childIndex:N}",
- DisplayName = $"TestChildResourceClaim{childIndex:N}",
- ResourceName = $"TestChildResourceClaim{childIndex:N}",
- Application = testApplication,
+ ClaimName = $"TestChildResourceClaim{resourceClaim.ClaimName}",
+ ResourceName = $"TestChildResourceClaim{resourceClaim.ClaimName}",
ParentResourceClaim = resourceClaim,
ParentResourceClaimId = resourceClaim.ResourceClaimId
}));
@@ -296,7 +257,7 @@ private IReadOnlyCollection SetupParentResourceClaimsWith
Save(parentResourceClaims.Cast